"""Router API pour les emplacements.""" from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.models.location import LocationType from app.repositories.location import LocationRepository from app.schemas.common import PaginatedResponse, SuccessResponse from app.schemas.location import ( LocationCreate, LocationResponse, LocationTree, LocationUpdate, LocationWithItemCount, ) router = APIRouter(prefix="/locations", tags=["Locations"]) @router.get("", response_model=PaginatedResponse[LocationResponse]) async def list_locations( page: int = 1, page_size: int = 50, parent_id: int | None = None, type: LocationType | None = None, db: AsyncSession = Depends(get_db), ) -> PaginatedResponse[LocationResponse]: """Liste les emplacements avec filtres optionnels.""" repo = LocationRepository(db) skip = (page - 1) * page_size filters = {} if parent_id is not None: filters["parent_id"] = parent_id if type is not None: filters["type"] = type locations = await repo.get_all(skip=skip, limit=page_size, **filters) total = await repo.count(**filters) items = [LocationResponse.model_validate(loc) for loc in locations] return PaginatedResponse.create(items=items, total=total, page=page, page_size=page_size) @router.get("/tree", response_model=list[LocationTree]) async def get_location_tree( db: AsyncSession = Depends(get_db), ) -> list[LocationTree]: """Récupère l'arborescence complète des emplacements.""" repo = LocationRepository(db) # Récupérer tous les emplacements all_locations = await repo.get_all(skip=0, limit=1000) # Construire un dictionnaire pour un accès rapide loc_dict: dict[int, LocationTree] = {} for loc in all_locations: loc_dict[loc.id] = LocationTree( id=loc.id, name=loc.name, type=loc.type, path=loc.path, children=[], item_count=0, ) # Construire l'arborescence roots: list[LocationTree] = [] for loc in all_locations: tree_node = loc_dict[loc.id] if loc.parent_id is None: roots.append(tree_node) elif loc.parent_id in loc_dict: loc_dict[loc.parent_id].children.append(tree_node) return roots @router.get("/roots", response_model=list[LocationResponse]) async def get_root_locations( db: AsyncSession = Depends(get_db), ) -> list[LocationResponse]: """Récupère les emplacements racine (pièces).""" repo = LocationRepository(db) locations = await repo.get_root_locations() return [LocationResponse.model_validate(loc) for loc in locations] @router.get("/{location_id}", response_model=LocationWithItemCount) async def get_location( location_id: int, db: AsyncSession = Depends(get_db), ) -> LocationWithItemCount: """Récupère un emplacement par son ID.""" repo = LocationRepository(db) result = await repo.get_with_item_count(location_id) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Emplacement {location_id} non trouvé", ) location, item_count = result return LocationWithItemCount( id=location.id, name=location.name, type=location.type, parent_id=location.parent_id, path=location.path, description=location.description, created_at=location.created_at, updated_at=location.updated_at, item_count=item_count, ) @router.get("/{location_id}/children", response_model=list[LocationResponse]) async def get_location_children( location_id: int, db: AsyncSession = Depends(get_db), ) -> list[LocationResponse]: """Récupère les enfants directs d'un emplacement.""" repo = LocationRepository(db) # Vérifier que le parent existe if not await repo.exists(location_id): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Emplacement {location_id} non trouvé", ) children = await repo.get_children(location_id) return [LocationResponse.model_validate(child) for child in children] @router.post("", response_model=LocationResponse, status_code=status.HTTP_201_CREATED) async def create_location( data: LocationCreate, db: AsyncSession = Depends(get_db), ) -> LocationResponse: """Crée un nouvel emplacement.""" repo = LocationRepository(db) # Vérifier que le parent existe si spécifié if data.parent_id is not None: if not await repo.exists(data.parent_id): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Emplacement parent {data.parent_id} non trouvé", ) location = await repo.create_with_path( name=data.name, type=data.type, parent_id=data.parent_id, description=data.description, ) await db.commit() return LocationResponse.model_validate(location) @router.put("/{location_id}", response_model=LocationResponse) async def update_location( location_id: int, data: LocationUpdate, db: AsyncSession = Depends(get_db), ) -> LocationResponse: """Met à jour un emplacement.""" repo = LocationRepository(db) # Vérifier que l'emplacement existe existing = await repo.get(location_id) if existing is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Emplacement {location_id} non trouvé", ) # Vérifier que le nouveau parent existe si spécifié if data.parent_id is not None and data.parent_id != existing.parent_id: if data.parent_id == location_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Un emplacement ne peut pas être son propre parent", ) if not await repo.exists(data.parent_id): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Emplacement parent {data.parent_id} non trouvé", ) # Mettre à jour location = await repo.update( location_id, name=data.name, type=data.type, parent_id=data.parent_id, description=data.description, ) # Recalculer les chemins si le nom ou le parent a changé if data.name or data.parent_id is not None: await repo.update_paths_recursive(location) await db.commit() return LocationResponse.model_validate(location) @router.delete("/{location_id}", response_model=SuccessResponse) async def delete_location( location_id: int, db: AsyncSession = Depends(get_db), ) -> SuccessResponse: """Supprime un emplacement.""" repo = LocationRepository(db) # Vérifier que l'emplacement existe result = await repo.get_with_item_count(location_id) if result is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Emplacement {location_id} non trouvé", ) location, item_count = result # Empêcher la suppression si des objets utilisent cet emplacement if item_count > 0: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Impossible de supprimer : {item_count} objet(s) utilisent cet emplacement", ) # Vérifier s'il y a des enfants children = await repo.get_children(location_id) if children: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Impossible de supprimer : cet emplacement a {len(children)} sous-emplacement(s)", ) await repo.delete(location_id) await db.commit() return SuccessResponse(message="Emplacement supprimé avec succès", id=location_id)