2026-07-11 18:17:07 +02:00
|
|
|
"""Equipment API router: list, detail, categories, image."""
|
|
|
|
|
import os
|
2026-07-09 01:36:46 +02:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
2026-07-11 18:17:07 +02:00
|
|
|
from fastapi.responses import FileResponse
|
2026-07-09 01:36:46 +02:00
|
|
|
from sqlalchemy import select, func, or_
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
from typing import Any
|
|
|
|
|
from app.database import get_db
|
|
|
|
|
from app.models.equipment import EquipmentCache
|
|
|
|
|
from app.schemas.equipment import EquipmentItem, EquipmentDetail, PaginatedResponse
|
|
|
|
|
from app.cache import cache
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/equipment", tags=["equipment"])
|
|
|
|
|
|
2026-07-11 18:17:07 +02:00
|
|
|
IMAGES_DIR = "/data/images/equipment"
|
|
|
|
|
|
2026-07-09 01:36:46 +02:00
|
|
|
|
|
|
|
|
@router.get("", response_model=PaginatedResponse)
|
|
|
|
|
async def list_equipment(
|
|
|
|
|
search: str | None = Query(None),
|
|
|
|
|
category: str | None = Query(None),
|
|
|
|
|
sort: str | None = Query("name_asc"),
|
|
|
|
|
page: int = Query(1, ge=1),
|
|
|
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
) -> Any:
|
|
|
|
|
"""Return paginated equipment list with optional search/filter/sort."""
|
|
|
|
|
cache_key = f"equipment:list:{search}:{category}:{sort}:{page}:{page_size}"
|
|
|
|
|
cached = await cache.get(cache_key)
|
|
|
|
|
if cached:
|
|
|
|
|
return cached
|
|
|
|
|
|
|
|
|
|
query = select(EquipmentCache)
|
|
|
|
|
count_query = select(func.count(EquipmentCache.id))
|
|
|
|
|
|
|
|
|
|
if search:
|
|
|
|
|
query = query.where(EquipmentCache.name.ilike(f"%{search}%"))
|
|
|
|
|
count_query = count_query.where(EquipmentCache.name.ilike(f"%{search}%"))
|
|
|
|
|
if category:
|
|
|
|
|
query = query.where(EquipmentCache.category == category)
|
|
|
|
|
count_query = count_query.where(EquipmentCache.category == category)
|
|
|
|
|
|
|
|
|
|
if sort == "name_desc":
|
|
|
|
|
query = query.order_by(EquipmentCache.name.desc())
|
|
|
|
|
else:
|
|
|
|
|
query = query.order_by(EquipmentCache.name.asc())
|
|
|
|
|
|
|
|
|
|
total_result = await db.execute(count_query)
|
|
|
|
|
total = total_result.scalar() or 0
|
|
|
|
|
|
|
|
|
|
offset = (page - 1) * page_size
|
|
|
|
|
result = await db.execute(query.offset(offset).limit(page_size))
|
|
|
|
|
items = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
response = {
|
2026-07-11 03:08:32 +02:00
|
|
|
"items": [EquipmentItem.model_validate(item).model_dump() for item in items],
|
2026-07-09 01:36:46 +02:00
|
|
|
"total": total,
|
|
|
|
|
"page": page,
|
|
|
|
|
"page_size": page_size,
|
|
|
|
|
"total_pages": (total + page_size - 1) // page_size if page_size > 0 else 0,
|
|
|
|
|
}
|
|
|
|
|
await cache.set(cache_key, response, ttl=3600)
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/categories", response_model=list[str])
|
|
|
|
|
async def list_categories(db: AsyncSession = Depends(get_db)) -> Any:
|
|
|
|
|
"""Return all distinct equipment categories."""
|
|
|
|
|
cached = await cache.get("equipment:categories")
|
|
|
|
|
if cached:
|
|
|
|
|
return cached
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(EquipmentCache.category).distinct().where(EquipmentCache.category.isnot(None))
|
|
|
|
|
)
|
|
|
|
|
categories = [row[0] for row in result.fetchall() if row[0]]
|
|
|
|
|
await cache.set("equipment:categories", categories, ttl=3600)
|
|
|
|
|
return categories
|
|
|
|
|
|
|
|
|
|
|
2026-07-11 18:17:07 +02:00
|
|
|
@router.get("/{equipment_id}/image")
|
|
|
|
|
async def get_equipment_image(equipment_id: int, db: AsyncSession = Depends(get_db)) -> Any:
|
|
|
|
|
"""Serve the locally stored image for an equipment item."""
|
|
|
|
|
image_path = os.path.join(IMAGES_DIR, f"{equipment_id}.jpg")
|
|
|
|
|
if os.path.exists(image_path):
|
|
|
|
|
return FileResponse(image_path, media_type="image/jpeg")
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
|
|
|
|
|
|
|
|
|
|
|
2026-07-09 01:36:46 +02:00
|
|
|
@router.get("/{equipment_id}", response_model=EquipmentDetail)
|
|
|
|
|
async def get_equipment(equipment_id: int, db: AsyncSession = Depends(get_db)) -> Any:
|
|
|
|
|
"""Return a single equipment detail by ID."""
|
|
|
|
|
cache_key = f"equipment:detail:{equipment_id}"
|
|
|
|
|
cached = await cache.get(cache_key)
|
|
|
|
|
if cached:
|
|
|
|
|
return cached
|
|
|
|
|
|
|
|
|
|
result = await db.execute(select(EquipmentCache).where(EquipmentCache.id == equipment_id))
|
|
|
|
|
item = result.scalar_one_or_none()
|
|
|
|
|
if not item:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found")
|
2026-07-11 03:08:32 +02:00
|
|
|
response = EquipmentDetail.model_validate(item).model_dump()
|
2026-07-11 18:17:07 +02:00
|
|
|
await cache.set(cache_key, response, ttl=3600)
|
2026-07-09 01:36:46 +02:00
|
|
|
return response
|