personal-lifecycle-tracker / backend / app / routers / items.py
items.py
Raw
from __future__ import annotations

from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status

from app.auth import get_current_user_optional
from app.models import (
    CreateItemDTO,
    LifecycleItem,
    LifecycleType,
    UpdateItemDTO,
    User,
)
from app.store import DatabaseStore, get_store
from app.telemetry import (
    record_item_completed,
    record_item_created,
    record_item_creation_failure,
    record_item_deleted,
)

router = APIRouter()


@router.get("", response_model=List[LifecycleItem], status_code=status.HTTP_200_OK)
async def list_items(
    groupId: Optional[str] = Query(None, description="Filter by groupId ('null' for personal items)"),
    type: Optional[LifecycleType] = Query(None, description="Filter by lifecycle type"),
    searchQuery: Optional[str] = Query(None, description="Search query string"),
    current_user: User = Depends(get_current_user_optional),
    store: DatabaseStore = Depends(get_store),
) -> List[LifecycleItem]:
    items = store.get_items(
        user=current_user,
        group_id=groupId,
        lifecycle_type=type,
        search_query=searchQuery,
    )
    return items


@router.post("", response_model=LifecycleItem, status_code=status.HTTP_201_CREATED)
async def create_item(
    payload: CreateItemDTO,
    current_user: User = Depends(get_current_user_optional),
    store: DatabaseStore = Depends(get_store),
) -> LifecycleItem:
    l_type = payload.lifecycleType.value if hasattr(payload.lifecycleType, "value") else str(payload.lifecycleType)
    if not payload.name or not payload.name.strip():
        record_item_creation_failure(l_type)
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Item name cannot be empty",
        )
    try:
        item = store.create_item(user=current_user, data=payload)
        record_item_created(item.lifecycleType.value)
        store.sync_metrics()
        return item
    except (ValueError, PermissionError) as exc:
        record_item_creation_failure(l_type)
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        )
    except Exception as exc:
        record_item_creation_failure(l_type)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Item creation failed: {exc}",
        )


@router.get("/{id}", response_model=LifecycleItem, status_code=status.HTTP_200_OK)
async def get_item(
    id: str,
    current_user: User = Depends(get_current_user_optional),
    store: DatabaseStore = Depends(get_store),
) -> LifecycleItem:
    item = store.get_item_by_id(id)
    if not item:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Item not found",
        )
    if not store.is_item_accessible(item, current_user):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Not authorized to access this item",
        )
    return item


@router.patch("/{id}", response_model=LifecycleItem, status_code=status.HTTP_200_OK)
async def update_item(
    id: str,
    payload: UpdateItemDTO,
    current_user: User = Depends(get_current_user_optional),
    store: DatabaseStore = Depends(get_store),
) -> LifecycleItem:
    item = store.get_item_by_id(id)
    if not item:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Item not found",
        )
    try:
        updated = store.update_item(user=current_user, item_id=id, data=payload)
        if not updated:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
        store.sync_metrics()
        return updated
    except (ValueError, PermissionError) as exc:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))


@router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(
    id: str,
    current_user: User = Depends(get_current_user_optional),
    store: DatabaseStore = Depends(get_store),
) -> Response:
    item = store.get_item_by_id(id)
    if not item:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Item not found",
        )
    try:
        deleted = store.delete_item(user=current_user, item_id=id)
        if not deleted:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
        record_item_deleted(item.lifecycleType.value)
        store.sync_metrics()
        return Response(status_code=status.HTTP_204_NO_CONTENT)
    except PermissionError as exc:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))


@router.post("/{id}/complete", response_model=LifecycleItem, status_code=status.HTTP_200_OK)
async def toggle_complete(
    id: str,
    current_user: User = Depends(get_current_user_optional),
    store: DatabaseStore = Depends(get_store),
) -> LifecycleItem:
    item = store.get_item_by_id(id)
    if not item:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Item not found",
        )
    try:
        updated = store.toggle_item_completion(id, user=current_user)
        if not updated:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="Item not found",
            )
        status_str = updated.status.value if hasattr(updated.status, "value") else str(updated.status)
        if status_str == "completed":
            record_item_completed(updated.lifecycleType.value)
        store.sync_metrics()
        return updated
    except PermissionError as exc:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc))