Files
OpenOPC/opc/presentation/kanban.py
T
2026-07-01 17:56:31 +08:00

158 lines
5.0 KiB
Python

"""Shared Kanban presentation helpers for UI and CLI surfaces."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Mapping
from opc.core.models import TaskStatus
STATUS_TO_COLUMN: dict[str, str] = {
"pending": "todo",
"running": "in-progress",
"idle": "in-progress",
"blocked": "in-progress",
"awaiting_peer": "in-progress",
"awaiting_manager_review": "in-progress",
"awaiting_human": "in-progress",
"awaiting_review": "in-progress",
"done": "done",
"failed": "done",
"cancelled": "done",
}
@dataclass(frozen=True)
class KanbanColumnDefinition:
column_id: str
name: str
sort_order: int
is_terminal: bool = False
color: str | None = None
DEFAULT_KANBAN_COLUMNS: tuple[KanbanColumnDefinition, ...] = (
KanbanColumnDefinition("todo", "Todo", 0, color="#6b7280"),
KanbanColumnDefinition("in-progress", "In Progress", 1, color="#eab308"),
KanbanColumnDefinition("done", "Done", 2, is_terminal=True, color="#22c55e"),
)
COMPANY_KANBAN_COLUMNS: tuple[KanbanColumnDefinition, ...] = (
KanbanColumnDefinition("todo", "To do", 0, color="#6b7280"),
KanbanColumnDefinition("in-progress", "In progress", 1, color="#eab308"),
KanbanColumnDefinition("in-review", "In review", 2, color="#f59e0b"),
KanbanColumnDefinition("done", "Done", 3, is_terminal=True, color="#22c55e"),
)
def column_to_task_status(column_id: str) -> TaskStatus | None:
normalized = str(column_id or "").strip().lower()
mapping = {
"todo": TaskStatus.PENDING,
"backlog": TaskStatus.PENDING,
"in-progress": TaskStatus.RUNNING,
"done": TaskStatus.DONE,
"blocked": TaskStatus.BLOCKED,
"failed": TaskStatus.FAILED,
"cancelled": TaskStatus.CANCELLED,
}
return mapping.get(normalized)
def task_status_value(task: Any) -> str:
status = getattr(task, "status", "")
return status.value if hasattr(status, "value") else str(status)
def priority_to_label(priority: int | None) -> str | None:
if priority is None:
return None
if priority <= 2:
return "urgent"
if priority <= 4:
return "high"
if priority <= 6:
return "medium"
if priority <= 8:
return "low"
return None
def datetime_to_timestamp(value: Any) -> float:
if hasattr(value, "timestamp"):
return float(value.timestamp())
if isinstance(value, datetime):
return float(value.timestamp())
return datetime.now().timestamp()
def build_board_columns(board_id: str, *, now: float | None = None) -> list[dict[str, Any]]:
timestamp = float(now) if now is not None else datetime.now().timestamp()
return [
{
"column_id": column.column_id,
"board_id": board_id,
"name": column.name,
"color": column.color,
"sort_order": column.sort_order,
"is_terminal": column.is_terminal,
"created_at": timestamp,
"updated_at": timestamp,
}
for column in DEFAULT_KANBAN_COLUMNS
]
def build_company_board_columns(board_id: str, *, now: float | None = None) -> list[dict[str, Any]]:
timestamp = float(now) if now is not None else datetime.now().timestamp()
return [
{
"column_id": column.column_id,
"board_id": board_id,
"name": column.name,
"color": column.color,
"sort_order": column.sort_order,
"is_terminal": column.is_terminal,
"created_at": timestamp,
"updated_at": timestamp,
}
for column in COMPANY_KANBAN_COLUMNS
]
def build_base_task_payload(
task: Any,
display_num: int,
*,
board_id: str | None = None,
assignee_ids: list[str] | None = None,
extra: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
status_val = task_status_value(task)
created_at = datetime_to_timestamp(getattr(task, "created_at", None))
updated_at = datetime_to_timestamp(getattr(task, "updated_at", None) or getattr(task, "created_at", None))
payload: dict[str, Any] = {
"task_id": getattr(task, "id", ""),
"display_id": f"OPC-{display_num}",
"board_id": board_id or getattr(task, "project_id", "default") or "default",
"column_id": STATUS_TO_COLUMN.get(status_val, "todo"),
"title": getattr(task, "title", ""),
"description": getattr(task, "description", "") or "",
"status": status_val,
"priority": priority_to_label(getattr(task, "priority", None)),
"assignee_ids": list(assignee_ids or []),
"tags": list(getattr(task, "tags", []) or []),
"sort_order": display_num,
"session_id": getattr(task, "session_id", None),
"chat_channel_id": f"session:{getattr(task, 'id', '')}",
"created_at": created_at,
"updated_at": updated_at,
"dependencies": list(getattr(task, "dependencies", []) or []),
}
if extra:
payload.update(dict(extra))
return payload