from datetime import timedelta from django.db.models import Count, F from django.utils import timezone from ninja import Router from ninja.errors import HttpError from apps.accounts.api import require_permission from apps.workitems.models import WorkItem from apps.workitems.schemas import WorkItemMetricsOut, WorkItemOut router = Router(tags=["tasks"]) def serialize_work_item(item: WorkItem) -> dict: now = timezone.now() if item.status != "active": completed_overdue = ( item.due_at and item.completed_at and item.completed_at > item.due_at ) sla_status = "completed_overdue" if completed_overdue else "completed" elif item.due_at and item.due_at <= now: sla_status = "overdue" elif item.due_at and item.due_at <= now + timedelta(hours=2): sla_status = "due_soon" else: sla_status = "on_track" return { "public_id": str(item.public_id), "task_type": item.task_type, "status": item.status, "object_type": item.object_type, "object_id": item.object_id, "ticket_id": item.ticket_id, "ticket_no": item.ticket.ticket_no if item.ticket_id else None, "title": item.title, "description": item.description, "payload": item.payload, "assignee_id": item.assignee_id, "created_at": item.created_at, "updated_at": item.updated_at, "completed_at": item.completed_at, "due_at": item.due_at, "sla_status": sla_status, } @router.get("/metrics", response=WorkItemMetricsOut) def get_work_item_metrics(request): require_permission(request, "ticket.view") now = timezone.now() active = WorkItem.objects.filter(status="active") completed = WorkItem.objects.filter(status="completed", completed_at__isnull=False) by_task_type = { item["task_type"]: item["total"] for item in active.values("task_type").annotate(total=Count("id")) } return { "active_count": active.count(), "overdue_count": active.filter(due_at__lte=now).count(), "due_soon_count": active.filter( due_at__gt=now, due_at__lte=now + timedelta(hours=2), ).count(), "completed_within_sla_count": completed.filter( completed_at__lte=F("due_at") ).count(), "completed_overdue_count": completed.filter( completed_at__gt=F("due_at") ).count(), "by_task_type": by_task_type, } @router.get("", response=list[WorkItemOut]) def list_work_items( request, status: str | None = "active", task_type: str | None = None, ticket_id: int | None = None, ): require_permission(request, "ticket.view") queryset = WorkItem.objects.select_related("ticket", "assignee") if status: queryset = queryset.filter(status=status) if task_type: queryset = queryset.filter(task_type=task_type) if ticket_id: queryset = queryset.filter(ticket_id=ticket_id) return [serialize_work_item(item) for item in queryset[:200]] @router.get("/{public_id}", response=WorkItemOut) def get_work_item(request, public_id: str): require_permission(request, "ticket.view") item = WorkItem.objects.select_related("ticket", "assignee").filter( public_id=public_id ).first() if item is None: raise HttpError(404, "任务不存在") return serialize_work_item(item)