diff --git a/be/app/api/v1/endpoints/lists.py b/be/app/api/v1/endpoints/lists.py
index 2b28884..e26bd14 100644
--- a/be/app/api/v1/endpoints/lists.py
+++ b/be/app/api/v1/endpoints/lists.py
@@ -12,7 +12,7 @@ from app.schemas.list import ListCreate, ListUpdate, ListPublic, ListDetail
from app.schemas.message import Message # For simple responses
from app.crud import list as crud_list
from app.crud import group as crud_group # Need for group membership check
-from app.schemas.list import ListStatus
+from app.schemas.list import ListStatus, ListStatusWithId
from app.schemas.expense import ExpensePublic # Import ExpensePublic
from app.core.exceptions import (
GroupMembershipError,
@@ -106,6 +106,39 @@ async def read_lists(
return lists
+@router.get(
+ "/statuses",
+ response_model=PyList[ListStatusWithId],
+ summary="Get Status for Multiple Lists",
+ tags=["Lists"]
+)
+async def read_lists_statuses(
+ ids: PyList[int] = Query(...),
+ db: AsyncSession = Depends(get_transactional_session),
+ current_user: UserModel = Depends(current_active_user),
+):
+ """
+ Retrieves the status for a list of lists.
+ - `updated_at`: The timestamp of the last update to the list itself.
+ - `item_count`: The total number of items in the list.
+ The user must have permission to view each list requested.
+ Lists that the user does not have permission for will be omitted from the response.
+ """
+ logger.info(f"User {current_user.email} requesting statuses for list IDs: {ids}")
+
+ statuses = await crud_list.get_lists_statuses_by_ids(db=db, list_ids=ids, user_id=current_user.id)
+
+ # The CRUD function returns a list of Row objects, so we map them to the Pydantic model
+ return [
+ ListStatusWithId(
+ id=s.id,
+ updated_at=s.updated_at,
+ item_count=s.item_count,
+ latest_item_updated_at=s.latest_item_updated_at
+ ) for s in statuses
+ ]
+
+
@router.get(
"/{list_id}",
response_model=ListDetail, # Return detailed list info including items
@@ -216,28 +249,13 @@ async def read_list_status(
current_user: UserModel = Depends(current_active_user),
):
"""
- Retrieves the completion status for a specific list
+ Retrieves the update timestamp and item count for a specific list
if the user has permission (creator or group member).
"""
logger.info(f"User {current_user.email} requesting status for list ID: {list_id}")
- list_db = await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
-
- # Calculate status
- total_items = len(list_db.items)
- completed_items = sum(1 for item in list_db.items if item.is_complete)
-
- try:
- completion_percentage = (completed_items / total_items * 100) if total_items > 0 else 0
- except ZeroDivisionError:
- completion_percentage = 0
-
- return ListStatus(
- list_id=list_db.id,
- total_items=total_items,
- completed_items=completed_items,
- completion_percentage=completion_percentage,
- last_updated=list_db.updated_at
- )
+ # The check_list_permission is not needed here as get_list_status handles not found
+ await crud_list.check_list_permission(db=db, list_id=list_id, user_id=current_user.id)
+ return await crud_list.get_list_status(db=db, list_id=list_id)
@router.get(
"/{list_id}/expenses",
diff --git a/be/app/crud/list.py b/be/app/crud/list.py
index 644a0e0..0aa1dbb 100644
--- a/be/app/crud/list.py
+++ b/be/app/crud/list.py
@@ -219,27 +219,27 @@ async def check_list_permission(db: AsyncSession, list_id: int, user_id: int, re
async def get_list_status(db: AsyncSession, list_id: int) -> ListStatus:
"""Gets the update timestamps and item count for a list."""
try:
- list_query = select(ListModel.updated_at).where(ListModel.id == list_id)
- list_result = await db.execute(list_query)
- list_updated_at = list_result.scalar_one_or_none()
+ query = (
+ select(
+ ListModel.updated_at,
+ sql_func.count(ItemModel.id).label("item_count"),
+ sql_func.max(ItemModel.updated_at).label("latest_item_updated_at")
+ )
+ .select_from(ListModel)
+ .outerjoin(ItemModel, ItemModel.list_id == ListModel.id)
+ .where(ListModel.id == list_id)
+ .group_by(ListModel.id)
+ )
+ result = await db.execute(query)
+ status = result.first()
- if list_updated_at is None:
+ if status is None:
raise ListNotFoundError(list_id)
- item_status_query = (
- select(
- sql_func.max(ItemModel.updated_at).label("latest_item_updated_at"),
- sql_func.count(ItemModel.id).label("item_count")
- )
- .where(ItemModel.list_id == list_id)
- )
- item_result = await db.execute(item_status_query)
- item_status = item_result.first()
-
return ListStatus(
- list_updated_at=list_updated_at,
- latest_item_updated_at=item_status.latest_item_updated_at if item_status else None,
- item_count=item_status.item_count if item_status else 0
+ updated_at=status.updated_at,
+ item_count=status.item_count,
+ latest_item_updated_at=status.latest_item_updated_at
)
except OperationalError as e:
raise DatabaseConnectionError(f"Failed to connect to database: {str(e)}")
@@ -295,4 +295,58 @@ async def get_list_by_name_and_group(
except OperationalError as e:
raise DatabaseConnectionError(f"Failed to connect to database: {str(e)}")
except SQLAlchemyError as e:
- raise DatabaseQueryError(f"Failed to query list by name and group: {str(e)}")
\ No newline at end of file
+ raise DatabaseQueryError(f"Failed to query list by name and group: {str(e)}")
+
+async def get_lists_statuses_by_ids(db: AsyncSession, list_ids: PyList[int], user_id: int) -> PyList[ListModel]:
+ """
+ Gets status for a list of lists if the user has permission.
+ Status includes list updated_at and a count of its items.
+ """
+ if not list_ids:
+ return []
+
+ try:
+ # First, get the groups the user is a member of
+ group_ids_result = await db.execute(
+ select(UserGroupModel.group_id).where(UserGroupModel.user_id == user_id)
+ )
+ user_group_ids = group_ids_result.scalars().all()
+
+ # Build the permission logic
+ permission_filter = or_(
+ # User is the creator of the list
+ and_(ListModel.created_by_id == user_id, ListModel.group_id.is_(None)),
+ # List belongs to a group the user is a member of
+ ListModel.group_id.in_(user_group_ids)
+ )
+
+ # Main query to get list data and item counts
+ query = (
+ select(
+ ListModel.id,
+ ListModel.updated_at,
+ sql_func.count(ItemModel.id).label("item_count"),
+ sql_func.max(ItemModel.updated_at).label("latest_item_updated_at")
+ )
+ .outerjoin(ItemModel, ListModel.id == ItemModel.list_id)
+ .where(
+ and_(
+ ListModel.id.in_(list_ids),
+ permission_filter
+ )
+ )
+ .group_by(ListModel.id)
+ )
+
+ result = await db.execute(query)
+
+ # The result will be rows of (id, updated_at, item_count).
+ # We need to verify that all requested list_ids that the user *should* have access to are present.
+ # The filter in the query already handles permissions.
+
+ return result.all() # Returns a list of Row objects with id, updated_at, item_count
+
+ except OperationalError as e:
+ raise DatabaseConnectionError(f"Failed to connect to database: {str(e)}")
+ except SQLAlchemyError as e:
+ raise DatabaseQueryError(f"Failed to get lists statuses: {str(e)}")
\ No newline at end of file
diff --git a/be/app/schemas/list.py b/be/app/schemas/list.py
index a2d4314..b21e506 100644
--- a/be/app/schemas/list.py
+++ b/be/app/schemas/list.py
@@ -42,6 +42,9 @@ class ListDetail(ListBase):
items: List[ItemPublic] = [] # Include list of items
class ListStatus(BaseModel):
- list_updated_at: datetime
- latest_item_updated_at: Optional[datetime] = None # Can be null if list has no items
- item_count: int
\ No newline at end of file
+ updated_at: datetime
+ item_count: int
+ latest_item_updated_at: Optional[datetime] = None
+
+class ListStatusWithId(ListStatus):
+ id: int
\ No newline at end of file
diff --git a/fe/src/pages/GroupDetailPage.vue b/fe/src/pages/GroupDetailPage.vue
index 970b4ed..231454d 100644
--- a/fe/src/pages/GroupDetailPage.vue
+++ b/fe/src/pages/GroupDetailPage.vue
@@ -81,10 +81,7 @@