7f7da15965
Completed: - Phase 0: Project Setup (T001-T003) - Docker Compose, FastAPI skeleton, React SPA - Phase 1: Auth System (T004-T008) - DB models, JWT auth, RBAC middleware, user management - Phase 2: Contacts & Tags (T009-T011) - CRUD API + UI - Phase 3: Equipment Catalog (T012-T014) - Models, API, UI with barcode/QR - Phase 4: Crew Management (T015-T017) - Models, availability, UI - Phase 5: Vehicle Fleet (T018-T020) - Models, assignments, UI - Phase 6: Projects (T021-T023) - Project hierarchy models, CRUD API, list/detail UI
194 lines
6.6 KiB
Python
194 lines
6.6 KiB
Python
"""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
|