"""Top-level, read-only scope fit guide at ``/scopes``. The site's primary view: a table of scopes with their specs, plus one column per non-Scope part type listing the accessories that fit each scope. Supports HTMX search/filter over scope attributes and required accessory fits. """ from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from fastapi.responses import HTMLResponse from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.crud import audit as audit_crud from app.crud import augmented_part as augmented_part_crud from app.crud import manufacturer as manufacturer_crud from app.crud import part_type as part_type_crud from app.crud import scope_model as scope_model_crud from app.crud import sub_part_type as sub_part_type_crud from app.models.user import User from app.services import scope_guide as scope_guide_service from app.web.admin import web_require from app.web.templates import templates router = APIRouter(include_in_schema=False) def _to_int(value: str) -> int | None: return int(value) if value.strip() else None def _can_write_scopes(user: User) -> bool: return user.is_superuser or "augmented_parts:write" in user.permissions async def _build(db: AsyncSession, filters: dict) -> dict: """Run the guide query for the given raw (string) filter values.""" return await scope_guide_service.build( db, description=filters["description"], manufacturer_id=_to_int(filters["manufacturer_id"]), scope_model_id=_to_int(filters["scope_model_id"]), sub_part_type_id=_to_int(filters["sub_part_type_id"]), fits_part_type_id=_to_int(filters["fits_part_type_id"]), ) async def _filter_options(db: AsyncSession) -> dict: """Dropdown option lists for the filter bar.""" scope_type = await part_type_crud.get_by_name( db, scope_guide_service.SCOPE_TYPE_NAME ) sub_part_types = [ spt for spt in await sub_part_type_crud.list_all(db, limit=1000) if scope_type is not None and spt.part_type_id == scope_type.id ] return { "manufacturers": await manufacturer_crud.list_all(db, limit=1000), "scope_models": await scope_model_crud.list_all(db, limit=1000), "sub_part_types": sub_part_types, } @router.get("/scopes", response_class=HTMLResponse) async def scopes_page( request: Request, description: str = "", manufacturer_id: str = "", scope_model_id: str = "", sub_part_type_id: str = "", fits_part_type_id: str = "", db: AsyncSession = Depends(get_db), user: User = Depends(web_require("augmented_parts:read", "fits:read")), ): filters = { "description": description, "manufacturer_id": manufacturer_id, "scope_model_id": scope_model_id, "sub_part_type_id": sub_part_type_id, "fits_part_type_id": fits_part_type_id, } guide = await _build(db, filters) return templates.TemplateResponse( request, "scopes.html", { "user": user, "filters": filters, "can_write_scopes": _can_write_scopes(user), **guide, **(await _filter_options(db)), }, ) @router.get("/scopes/filter", response_class=HTMLResponse) async def scopes_filter( request: Request, description: str = "", manufacturer_id: str = "", scope_model_id: str = "", sub_part_type_id: str = "", fits_part_type_id: str = "", db: AsyncSession = Depends(get_db), user: User = Depends(web_require("augmented_parts:read", "fits:read")), ): filters = { "description": description, "manufacturer_id": manufacturer_id, "scope_model_id": scope_model_id, "sub_part_type_id": sub_part_type_id, "fits_part_type_id": fits_part_type_id, } guide = await _build(db, filters) return templates.TemplateResponse( request, "_scopes_table_card.html", {**guide, "can_write_scopes": _can_write_scopes(user)}, ) @router.delete("/scopes/{augmented_part_id}") async def delete_scope( augmented_part_id: int, db: AsyncSession = Depends(get_db), actor: User = Depends(web_require("augmented_parts:write")), ): ap = await augmented_part_crud.get(db, augmented_part_id) if ap is None: raise HTTPException(status_code=404, detail="Scope not found") label = ap.sku or ap.description await augmented_part_crud.delete(db, ap) await audit_crud.log( db, actor=actor, action="delete", resource_type="AugmentedPart", resource_id=augmented_part_id, description=audit_crud.describe_delete( "AugmentedPart", augmented_part_id, label ), ) return Response(status_code=status.HTTP_200_OK)