102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
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)
|