"""Equipment CRUD endpoints.""" from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy import select, func, or_ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload from app.api.deps import get_current_user, require_permission from app.db.session import get_async_session from app.models import User, Equipment, StockLocation, Contact from app.schemas.equipment import ( EquipmentCreateRequest, EquipmentUpdateRequest, EquipmentResponse, EquipmentListResponse, LocationRef, SupplierRef, ) router = APIRouter(prefix="/equipment", tags=["equipment"]) @router.get("", response_model=EquipmentListResponse) async def list_equipment( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), search: str | None = Query(None, description="Search in name, brand, serial_number"), category: str | None = Query(None, description="Filter by category"), status: str | None = Query(None, description="Filter by status"), location_id: str | None = Query(None, description="Filter by location"), current_user: User = Depends(require_permission("equipment:read")), session: AsyncSession = Depends(get_async_session), ): """List equipment with search, filters, pagination.""" account_id = current_user.account_id base_q = select(Equipment).where(Equipment.account_id == account_id) if search: pattern = f"%{search}%" base_q = base_q.where( or_( Equipment.name.ilike(pattern), Equipment.brand.ilike(pattern), Equipment.serial_number.ilike(pattern), Equipment.barcode.ilike(pattern), ) ) if category: base_q = base_q.where(Equipment.category == category) if status: base_q = base_q.where(Equipment.status == status) if location_id: base_q = base_q.where(Equipment.location_id == location_id) count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 q = ( base_q .options(joinedload(Equipment.location), joinedload(Equipment.supplier)) .order_by(Equipment.updated_at.desc()) .offset((page - 1) * size) .limit(size) ) result = await session.execute(q) items = result.unique().scalars().all() return EquipmentListResponse( items=[EquipmentResponse.model_validate(i) for i in items], total=total, page=page, size=size, ) @router.post("", response_model=EquipmentResponse, status_code=status.HTTP_201_CREATED) async def create_equipment( body: EquipmentCreateRequest, current_user: User = Depends(require_permission("equipment:write")), session: AsyncSession = Depends(get_async_session), ): """Create a new equipment item.""" account_id = current_user.account_id # Validate location if provided if body.location_id: loc_result = await session.execute( select(StockLocation).where( StockLocation.id == body.location_id, StockLocation.account_id == account_id, ) ) if not loc_result.scalars().first(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found", ) # Validate supplier if provided if body.supplier_id: sup_result = await session.execute( select(Contact).where( Contact.id == body.supplier_id, Contact.account_id == account_id, ) ) if not sup_result.scalars().first(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Supplier (contact) not found", ) item = Equipment(account_id=account_id, **body.model_dump()) session.add(item) await session.commit() await session.refresh(item) await session.execute( select(Equipment) .options(joinedload(Equipment.location), joinedload(Equipment.supplier)) .where(Equipment.id == item.id) ) await session.refresh(item) return EquipmentResponse.model_validate(item) @router.get("/{item_id}", response_model=EquipmentResponse) async def get_equipment( item_id: str, current_user: User = Depends(require_permission("equipment:read")), session: AsyncSession = Depends(get_async_session), ): """Get a specific equipment item.""" account_id = current_user.account_id result = await session.execute( select(Equipment) .options(joinedload(Equipment.location), joinedload(Equipment.supplier)) .where(Equipment.id == item_id, Equipment.account_id == account_id) ) item = result.unique().scalars().first() if not item: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found") return EquipmentResponse.model_validate(item) @router.put("/{item_id}", response_model=EquipmentResponse) async def update_equipment( item_id: str, body: EquipmentUpdateRequest, current_user: User = Depends(require_permission("equipment:write")), session: AsyncSession = Depends(get_async_session), ): """Update an existing equipment item.""" account_id = current_user.account_id result = await session.execute( select(Equipment) .options(joinedload(Equipment.location), joinedload(Equipment.supplier)) .where(Equipment.id == item_id, Equipment.account_id == account_id) ) item = result.unique().scalars().first() if not item: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found") update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(item, field, value) await session.commit() await session.refresh(item) return EquipmentResponse.model_validate(item) @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_equipment( item_id: str, current_user: User = Depends(require_permission("equipment:delete")), session: AsyncSession = Depends(get_async_session), ): """Delete an equipment item.""" account_id = current_user.account_id result = await session.execute( select(Equipment).where(Equipment.id == item_id, Equipment.account_id == account_id) ) item = result.scalars().first() if not item: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found") await session.delete(item) await session.commit() return None