# be/app/crud/schedule.py import logging from datetime import date, timedelta from typing import List from itertools import cycle from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from app.models import Chore, Group, User, ChoreAssignment, UserGroup, ChoreTypeEnum, ChoreHistoryEventTypeEnum from app.crud.group import get_group_by_id from app.crud.history import create_chore_history_entry from app.core.exceptions import GroupNotFoundError, ChoreOperationError logger = logging.getLogger(__name__) async def generate_group_chore_schedule( db: AsyncSession, *, group_id: int, start_date: date, end_date: date, user_id: int, # The user initiating the action member_ids: List[int] = None ) -> List[ChoreAssignment]: """ Generates a round-robin chore schedule for all group chores within a date range. """ if start_date > end_date: raise ChoreOperationError("Start date cannot be after end date.") group = await get_group_by_id(db, group_id) if not group: raise GroupNotFoundError(group_id) if not member_ids: # If no members are specified, use all members from the group members_result = await db.execute( select(UserGroup.user_id).where(UserGroup.group_id == group_id) ) member_ids = members_result.scalars().all() if not member_ids: raise ChoreOperationError("Cannot generate schedule with no members.") # Fetch all chores belonging to this group chores_result = await db.execute( select(Chore).where(Chore.group_id == group_id, Chore.type == ChoreTypeEnum.group) ) group_chores = chores_result.scalars().all() if not group_chores: logger.info(f"No chores found in group {group_id} to generate a schedule for.") return [] member_cycle = cycle(member_ids) new_assignments = [] current_date = start_date while current_date <= end_date: for chore in group_chores: # Check if a chore is due on the current day based on its frequency # This is a simplified check. A more robust system would use the chore's next_due_date # and frequency to see if it falls on the current_date. # For this implementation, we assume we generate assignments for ALL chores on ALL days # in the range, which might not be desired. # A better approach is needed here. Let's assume for now we just create assignments for each chore # on its *next* due date if it falls within the range. if start_date <= chore.next_due_date <= end_date: # Check if an assignment for this chore on this due date already exists existing_assignment_result = await db.execute( select(ChoreAssignment.id) .where(ChoreAssignment.chore_id == chore.id, ChoreAssignment.due_date == chore.next_due_date) .limit(1) ) if existing_assignment_result.scalar_one_or_none(): logger.info(f"Skipping assignment for chore '{chore.name}' on {chore.next_due_date} as it already exists.") continue assigned_to_user_id = next(member_cycle) assignment = ChoreAssignment( chore_id=chore.id, assigned_to_user_id=assigned_to_user_id, due_date=chore.next_due_date, # Assign on the chore's own next_due_date is_complete=False ) db.add(assignment) new_assignments.append(assignment) logger.info(f"Created assignment for chore '{chore.name}' to user {assigned_to_user_id} on {chore.next_due_date}") current_date += timedelta(days=1) if not new_assignments: logger.info(f"No new assignments were generated for group {group_id} in the specified date range.") return [] # Log a single group-level event for the schedule generation await create_chore_history_entry( db, chore_id=None, # This is a group-level event group_id=group_id, changed_by_user_id=user_id, event_type=ChoreHistoryEventTypeEnum.SCHEDULE_GENERATED, event_data={ "start_date": start_date.isoformat(), "end_date": end_date.isoformat(), "member_ids": member_ids, "assignments_created": len(new_assignments) } ) await db.flush() # Refresh assignments to load relationships if needed, although not strictly necessary # as the objects are already in the session. for assign in new_assignments: await db.refresh(assign) return new_assignments