Files
leocrm/app/plugins/builtins/tasks/services.py
T

213 lines
6.5 KiB
Python
Raw Normal View History

"""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]