158 lines
5.0 KiB
Python
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
|