"""EquipmentGroup (Bundle) CRUD endpoints.""" from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload, selectinload from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, EquipmentGroup, Equipment from app.models.equipment_group import equipment_group_items from app.schemas.equipment_group import ( EquipmentGroupCreateRequest, EquipmentGroupUpdateRequest, EquipmentGroupResponse, EquipmentGroupListResponse, ) router = APIRouter(prefix="/equipment-groups", tags=["equipment-groups"]) def _load_items(group: EquipmentGroup) -> list[dict]: """Build items list from M2M relationship.""" # The items relationship loads Equipment objects; we need quantity from the association # We'll use a separate query approach in the endpoints. return [] @router.get("", response_model=EquipmentGroupListResponse) async def list_equipment_groups( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), search: str | None = Query(None, description="Search in name"), current_user: User = Depends(require_permission("equipment:read")), session: AsyncSession = Depends(get_async_session), ): """List equipment groups with search and pagination.""" account_id = current_user.account_id base_q = select(EquipmentGroup).where(EquipmentGroup.account_id == account_id) if search: pattern = f"%{search}%" base_q = base_q.where(EquipmentGroup.name.ilike(pattern)) count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 q = ( base_q.options( joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items), ) .order_by(EquipmentGroup.name) .offset((page - 1) * size) .limit(size) ) result = await session.execute(q) groups = result.unique().scalars().all() return EquipmentGroupListResponse( items=[EquipmentGroupResponse.model_validate(g) for g in groups], total=total, page=page, size=size, ) @router.post( "", response_model=EquipmentGroupResponse, status_code=status.HTTP_201_CREATED ) async def create_equipment_group( body: EquipmentGroupCreateRequest, current_user: User = Depends(require_permission("equipment:write")), session: AsyncSession = Depends(get_async_session), ): """Create a new equipment group (bundle).""" account_id = current_user.account_id group = EquipmentGroup( account_id=account_id, name=body.name, description=body.description, daily_rate=body.daily_rate, default_location_id=body.default_location_id, ) if body.items: equipment_ids = [i.equipment_id for i in body.items] eq_result = await session.execute( select(Equipment).where( Equipment.id.in_(equipment_ids), Equipment.account_id == account_id, ) ) items = eq_result.scalars().all() group.items = items # Store quantities via the association table directly for item_schema in body.items: await session.execute( equipment_group_items.update() .where( equipment_group_items.c.group_id == group.id, equipment_group_items.c.equipment_id == item_schema.equipment_id, ) .values(quantity=item_schema.quantity) ) session.add(group) await session.commit() await session.refresh(group) # Reload with relationships await session.execute( select(EquipmentGroup) .options( joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items), ) .where(EquipmentGroup.id == group.id) ) await session.refresh(group) return EquipmentGroupResponse.model_validate(group) @router.get("/{group_id}", response_model=EquipmentGroupResponse) async def get_equipment_group( group_id: str, current_user: User = Depends(require_permission("equipment:read")), session: AsyncSession = Depends(get_async_session), ): """Get a specific equipment group with items.""" account_id = current_user.account_id result = await session.execute( select(EquipmentGroup) .options( joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items), ) .where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id) ) group = result.unique().scalars().first() if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found" ) return EquipmentGroupResponse.model_validate(group) @router.put("/{group_id}", response_model=EquipmentGroupResponse) async def update_equipment_group( group_id: str, body: EquipmentGroupUpdateRequest, current_user: User = Depends(require_permission("equipment:write")), session: AsyncSession = Depends(get_async_session), ): """Update an equipment group.""" account_id = current_user.account_id result = await session.execute( select(EquipmentGroup) .options( joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items), ) .where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id) ) group = result.unique().scalars().first() if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found" ) update_data = body.model_dump(exclude_unset=True, exclude={"items"}) for field, value in update_data.items(): setattr(group, field, value) # Handle items replacement if body.items is not None: equipment_ids = [i.equipment_id for i in body.items] eq_result = await session.execute( select(Equipment).where( Equipment.id.in_(equipment_ids), Equipment.account_id == account_id, ) ) group.items = eq_result.scalars().all() # Update quantities for item_schema in body.items: await session.execute( equipment_group_items.update() .where( equipment_group_items.c.group_id == group.id, equipment_group_items.c.equipment_id == item_schema.equipment_id, ) .values(quantity=item_schema.quantity) ) await session.commit() await session.refresh(group) return EquipmentGroupResponse.model_validate(group) @router.delete("/{group_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_equipment_group( group_id: str, current_user: User = Depends(require_permission("equipment:delete")), session: AsyncSession = Depends(get_async_session), ): """Delete an equipment group.""" account_id = current_user.account_id result = await session.execute( select(EquipmentGroup).where( EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id, ) ) group = result.scalars().first() if not group: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found" ) await session.delete(group) await session.commit() return None