"""Business logic for the Tasks plugin.""" from __future__ import annotations import uuid from datetime import datetime, timezone from typing import Any from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.plugins.builtins.tasks.models import Task def _task_to_dict(task: Task) -> dict[str, Any]: """Serialize a Task model to a dict.""" return { "id": str(task.id), "title": task.title, "description": task.description, "status": task.status, "priority": task.priority, "due_date": task.due_date.isoformat() if task.due_date else None, "assigned_to": str(task.assigned_to) if task.assigned_to else None, "contact_id": str(task.contact_id) if task.contact_id else None, "created_by": str(task.created_by) if task.created_by else None, "created_at": task.created_at.isoformat() if task.created_at else None, "updated_at": task.updated_at.isoformat() if task.updated_at else None, } async def list_tasks( db: AsyncSession, tenant_id: uuid.UUID, *, page: int = 1, page_size: int = 25, status: str | None = None, priority: str | None = None, assigned_to: str | None = None, contact_id: str | None = None, search: str | None = None, ) -> dict[str, Any]: """List tasks with filtering and pagination.""" query = select(Task).where(Task.tenant_id == tenant_id, Task.deleted_at.is_(None)) if status: query = query.where(Task.status == status) if priority: query = query.where(Task.priority == priority) if assigned_to: query = query.where(Task.assigned_to == uuid.UUID(assigned_to)) if contact_id: query = query.where(Task.contact_id == uuid.UUID(contact_id)) if search: query = query.where(Task.title.ilike(f"%{search}%")) # Count total count_query = select(func.count()).select_from(query.subquery()) total_result = await db.execute(count_query) total = total_result.scalar() or 0 # Paginate query = query.order_by(Task.due_date.asc().nulls_last(), Task.created_at.desc()) query = query.offset((page - 1) * page_size).limit(page_size) result = await db.execute(query) tasks = result.scalars().all() return { "items": [_task_to_dict(t) for t in tasks], "total": total, "page": page, "page_size": page_size, } async def get_task(db: AsyncSession, tenant_id: uuid.UUID, task_id: uuid.UUID) -> dict[str, Any] | None: """Get a single task by ID.""" result = await db.execute( select(Task).where(Task.id == task_id, Task.tenant_id == tenant_id, Task.deleted_at.is_(None)) ) task = result.scalar_one_or_none() if task is None: return None return _task_to_dict(task) async def create_task( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, data: dict[str, Any], ) -> dict[str, Any]: """Create a new task.""" task = Task( tenant_id=tenant_id, title=data["title"], description=data.get("description"), status=data.get("status", "open"), priority=data.get("priority", "medium"), due_date=data.get("due_date"), assigned_to=uuid.UUID(data["assigned_to"]) if data.get("assigned_to") else None, contact_id=uuid.UUID(data["contact_id"]) if data.get("contact_id") else None, created_by=user_id, ) db.add(task) await db.flush() return _task_to_dict(task) async def update_task( db: AsyncSession, tenant_id: uuid.UUID, task_id: uuid.UUID, data: dict[str, Any], ) -> dict[str, Any] | None: """Update a task.""" result = await db.execute( select(Task).where(Task.id == task_id, Task.tenant_id == tenant_id, Task.deleted_at.is_(None)) ) task = result.scalar_one_or_none() if task is None: return None if "title" in data and data["title"] is not None: task.title = data["title"] if "description" in data: task.description = data["description"] if "status" in data and data["status"] is not None: task.status = data["status"] if "priority" in data and data["priority"] is not None: task.priority = data["priority"] if "due_date" in data: task.due_date = data["due_date"] if "assigned_to" in data: task.assigned_to = uuid.UUID(data["assigned_to"]) if data["assigned_to"] else None if "contact_id" in data: task.contact_id = uuid.UUID(data["contact_id"]) if data["contact_id"] else None await db.flush() return _task_to_dict(task) async def delete_task(db: AsyncSession, tenant_id: uuid.UUID, task_id: uuid.UUID) -> bool: """Soft-delete a task.""" result = await db.execute( select(Task).where(Task.id == task_id, Task.tenant_id == tenant_id, Task.deleted_at.is_(None)) ) task = result.scalar_one_or_none() if task is None: return False task.deleted_at = datetime.now(timezone.utc) await db.flush() return True async def assign_task( db: AsyncSession, tenant_id: uuid.UUID, task_id: uuid.UUID, assigned_to: uuid.UUID, ) -> dict[str, Any] | None: """Assign a task to a user.""" result = await db.execute( select(Task).where(Task.id == task_id, Task.tenant_id == tenant_id, Task.deleted_at.is_(None)) ) task = result.scalar_one_or_none() if task is None: return None task.assigned_to = assigned_to await db.flush() return _task_to_dict(task) async def update_task_status( db: AsyncSession, tenant_id: uuid.UUID, task_id: uuid.UUID, new_status: str, ) -> dict[str, Any] | None: """Update task status.""" result = await db.execute( select(Task).where(Task.id == task_id, Task.tenant_id == tenant_id, Task.deleted_at.is_(None)) ) task = result.scalar_one_or_none() if task is None: return None task.status = new_status await db.flush() return _task_to_dict(task) async def get_due_tasks( db: AsyncSession, tenant_id: uuid.UUID, *, before: datetime | None = None, ) -> list[dict[str, Any]]: """Get tasks that are due (for ARQ reminder job).""" now = before or datetime.now(timezone.utc) result = await db.execute( select(Task).where( Task.tenant_id == tenant_id, Task.deleted_at.is_(None), Task.status != "done", Task.due_date.is_not(None), Task.due_date <= now, ) ) tasks = result.scalars().all() return [_task_to_dict(t) for t in tasks]