"""Vehicle CRUD endpoints including assignments.""" from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy import select, func, or_ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.api.deps import require_permission from app.db.session import get_async_session from app.models import User, Vehicle, VehicleAssignment from app.schemas.vehicle import ( VehicleCreateRequest, VehicleUpdateRequest, VehicleResponse, VehicleListResponse, VehicleAssignmentCreateRequest, VehicleAssignmentUpdateRequest, VehicleAssignmentResponse, VehicleAssignmentListResponse, ) router = APIRouter(prefix="/vehicles", tags=["vehicles"]) assign_router = APIRouter(prefix="/vehicle-assignments", tags=["vehicle-assignments"]) # ===== Vehicle Endpoints ===== @router.get("", response_model=VehicleListResponse) async def list_vehicles( page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), search: str | None = Query(None, description="Search in name, plate, brand"), vehicle_type: str | None = Query(None), is_active: bool | None = Query(None), current_user: User = Depends(require_permission("vehicles:read")), session: AsyncSession = Depends(get_async_session), ): account_id = current_user.account_id base_q = select(Vehicle).where(Vehicle.account_id == account_id) if search: pattern = f"%{search}%" base_q = base_q.where( or_( Vehicle.name.ilike(pattern), Vehicle.license_plate.ilike(pattern), Vehicle.brand.ilike(pattern), Vehicle.model.ilike(pattern), ) ) if vehicle_type: base_q = base_q.where(Vehicle.vehicle_type == vehicle_type) if is_active is not None: base_q = base_q.where(Vehicle.is_active == is_active) count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 q = ( base_q.options(selectinload(Vehicle.assignments)) .order_by(Vehicle.name) .offset((page - 1) * size) .limit(size) ) result = await session.execute(q) items = result.unique().scalars().all() return VehicleListResponse( items=[VehicleResponse.model_validate(i) for i in items], total=total, page=page, size=size, ) @router.post("", response_model=VehicleResponse, status_code=status.HTTP_201_CREATED) async def create_vehicle( body: VehicleCreateRequest, current_user: User = Depends(require_permission("vehicles:write")), session: AsyncSession = Depends(get_async_session), ): account_id = current_user.account_id vehicle = Vehicle(account_id=account_id, **body.model_dump()) session.add(vehicle) await session.commit() await session.refresh(vehicle) return VehicleResponse.model_validate(vehicle) @router.get("/{vehicle_id}", response_model=VehicleResponse) async def get_vehicle( vehicle_id: str, current_user: User = Depends(require_permission("vehicles:read")), session: AsyncSession = Depends(get_async_session), ): account_id = current_user.account_id result = await session.execute( select(Vehicle) .options(selectinload(Vehicle.assignments)) .where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id) ) vehicle = result.unique().scalars().first() if not vehicle: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found" ) return VehicleResponse.model_validate(vehicle) @router.put("/{vehicle_id}", response_model=VehicleResponse) async def update_vehicle( vehicle_id: str, body: VehicleUpdateRequest, current_user: User = Depends(require_permission("vehicles:write")), session: AsyncSession = Depends(get_async_session), ): account_id = current_user.account_id result = await session.execute( select(Vehicle) .options(selectinload(Vehicle.assignments)) .where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id) ) vehicle = result.unique().scalars().first() if not vehicle: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found" ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(vehicle, field, value) await session.commit() await session.refresh(vehicle) return VehicleResponse.model_validate(vehicle) @router.delete("/{vehicle_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_vehicle( vehicle_id: str, current_user: User = Depends(require_permission("vehicles:delete")), session: AsyncSession = Depends(get_async_session), ): account_id = current_user.account_id result = await session.execute( select(Vehicle).where( Vehicle.id == vehicle_id, Vehicle.account_id == account_id ) ) vehicle = result.scalars().first() if not vehicle: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found" ) await session.delete(vehicle) await session.commit() return None # ===== VehicleAssignment Endpoints ===== @assign_router.get("", response_model=VehicleAssignmentListResponse) async def list_assignments( page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), vehicle_id: str | None = Query(None), status: str | None = Query(None), current_user: User = Depends(require_permission("vehicles:read")), session: AsyncSession = Depends(get_async_session), ): account_id = current_user.account_id base_q = ( select(VehicleAssignment).join(Vehicle).where(Vehicle.account_id == account_id) ) if vehicle_id: base_q = base_q.where(VehicleAssignment.vehicle_id == vehicle_id) if status: base_q = base_q.where(VehicleAssignment.status == status) count_q = select(func.count()).select_from(base_q.subquery()) total = (await session.execute(count_q)).scalar() or 0 q = ( base_q.order_by(VehicleAssignment.start_date.desc()) .offset((page - 1) * size) .limit(size) ) result = await session.execute(q) items = result.scalars().all() return VehicleAssignmentListResponse( items=[VehicleAssignmentResponse.model_validate(i) for i in items], total=total, page=page, size=size, ) @assign_router.post( "", response_model=VehicleAssignmentResponse, status_code=status.HTTP_201_CREATED ) async def create_assignment( body: VehicleAssignmentCreateRequest, current_user: User = Depends(require_permission("vehicles:write")), session: AsyncSession = Depends(get_async_session), ): v_result = await session.execute( select(Vehicle).where( Vehicle.id == body.vehicle_id, Vehicle.account_id == current_user.account_id ) ) if not v_result.scalars().first(): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found" ) assign = VehicleAssignment(**body.model_dump()) session.add(assign) await session.commit() await session.refresh(assign) return VehicleAssignmentResponse.model_validate(assign) @assign_router.put("/{assign_id}", response_model=VehicleAssignmentResponse) async def update_assignment( assign_id: str, body: VehicleAssignmentUpdateRequest, current_user: User = Depends(require_permission("vehicles:write")), session: AsyncSession = Depends(get_async_session), ): result = await session.execute( select(VehicleAssignment) .join(Vehicle) .where( VehicleAssignment.id == assign_id, Vehicle.account_id == current_user.account_id, ) ) assign = result.scalars().first() if not assign: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found" ) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(assign, field, value) await session.commit() await session.refresh(assign) return VehicleAssignmentResponse.model_validate(assign) @assign_router.delete("/{assign_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_assignment( assign_id: str, current_user: User = Depends(require_permission("vehicles:delete")), session: AsyncSession = Depends(get_async_session), ): result = await session.execute( select(VehicleAssignment) .join(Vehicle) .where( VehicleAssignment.id == assign_id, Vehicle.account_id == current_user.account_id, ) ) assign = result.scalars().first() if not assign: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found" ) await session.delete(assign) await session.commit() return None