"""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, Form, Request 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.pagination import paginate from app.web.templates import templates router = APIRouter(include_in_schema=False) _PAGE_SIZE = 50 _TABLE_TARGET = "#scopes-table-card" _FILTER_PATH = "/scopes/filter" 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. Always returns the full matching row set — pagination is applied by the caller by slicing ``rows`` (see ``_paginated_guide``). """ 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 _paginated_guide( db: AsyncSession, filters: dict, page: int, page_size: int ) -> tuple[dict, dict]: """Run the guide query, then slice its rows to the requested page. Returns ``(guide, pagination)`` where ``guide["rows"]`` is already sliced. """ guide = await _build(db, filters) all_rows = guide["rows"] pagination = paginate( _FILTER_PATH, filters, page, page_size, len(all_rows), _TABLE_TARGET ) guide["rows"] = all_rows[pagination["skip"] : pagination["skip"] + page_size] return guide, pagination 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, pagination = await _paginated_guide(db, filters, 1, _PAGE_SIZE) return templates.TemplateResponse( request, "scopes.html", { "user": user, "filters": filters, "can_write_scopes": _can_write_scopes(user), "pagination": pagination, **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 = "", page: int = 1, page_size: int = _PAGE_SIZE, 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, pagination = await _paginated_guide(db, filters, page, page_size) return templates.TemplateResponse( request, "_scopes_table_card.html", { **guide, "pagination": pagination, "can_write_scopes": _can_write_scopes(user), }, ) @router.post("/scopes/bulk-delete", response_class=HTMLResponse) async def bulk_delete_scopes( request: Request, scope_ids: list[int] = Form([]), description: str = Form(""), manufacturer_id: str = Form(""), scope_model_id: str = Form(""), sub_part_type_id: str = Form(""), fits_part_type_id: str = Form(""), db: AsyncSession = Depends(get_db), actor: User = Depends(web_require("augmented_parts:write")), ): for augmented_part_id in scope_ids: ap = await augmented_part_crud.get(db, augmented_part_id) if ap is None: continue 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 ), ) 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, pagination = await _paginated_guide(db, filters, 1, _PAGE_SIZE) return templates.TemplateResponse( request, "_scopes_table_card.html", { **guide, "pagination": pagination, "can_write_scopes": _can_write_scopes(actor), }, )