Files
nas-tools/dashboard/backend/routers/opc_tasks.py
T
Gan, Jimmy 1ca019fa48
Deploy Dashboard (Dev) / deploy-dev (push) Has been cancelled
Run Tests / Backend Tests (push) Has been cancelled
Run Tests / Frontend Tests (push) Has been cancelled
Run Tests / Test Summary (push) Has been cancelled
feat: add OPC (One Person Company) management system with Kanban board
Phase 1 MVP implementation:
- PostgreSQL database schema for tasks, agents, executions, time tracking
- Backend API: task CRUD, agent management, automatic time tracking
- Frontend: Kanban board with drag-and-drop (Parking Lot → In Progress → Done)
- Task modal for creating/editing tasks with tags, priority, assignee
- 3 core agents seeded: PM, CTO, COO
- Agent approval workflow: CxO level requires approval, others auto-execute
- Automatic time tracking: starts on in_progress, stops on done
- RBAC integration: OPC page accessible to admin/member roles

Features:
- Drag-and-drop tasks between columns
- Assign tasks to humans or AI agents
- Priority badges (low, medium, high, urgent)
- Tags and due dates
- Task history tracking
- Time entry tracking with automatic timer
- Agent execution records with approval workflow

Next steps:
- Initialize OPC database on NAS
- Implement agent executor service
- Add WebSocket for real-time updates
- Add email notifications
- Add PDF invoice generation
2026-03-31 14:31:31 +08:00

260 lines
7.0 KiB
Python

"""
OPC Task Management Router
"""
from fastapi import APIRouter, Depends, Query, HTTPException
from typing import Optional, List
from pydantic import BaseModel
from datetime import datetime
from db import opc_db
from rbac import require_page, User, _inject_user
router = APIRouter()
class TaskCreate(BaseModel):
title: str
description: Optional[str] = None
status: str = "parking_lot"
priority: str = "medium"
project_id: Optional[int] = None
assigned_to: Optional[str] = None
assigned_type: str = "human"
tags: Optional[List[str]] = None
due_date: Optional[datetime] = None
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = None
priority: Optional[str] = None
project_id: Optional[int] = None
assigned_to: Optional[str] = None
assigned_type: Optional[str] = None
tags: Optional[List[str]] = None
due_date: Optional[datetime] = None
completed_at: Optional[datetime] = None
class TaskMove(BaseModel):
status: str
class TaskAssign(BaseModel):
assigned_to: str
assigned_type: str = "human"
@router.get("/tasks")
async def list_tasks(
status: Optional[str] = Query(None),
assigned_to: Optional[str] = Query(None),
project_id: Optional[int] = Query(None),
priority: Optional[str] = Query(None),
tags: Optional[str] = Query(None),
limit: int = Query(50, le=100),
offset: int = Query(0, ge=0),
user: User = Depends(_inject_user)
):
"""List tasks with filters"""
tag_list = tags.split(",") if tags else None
tasks = await opc_db.get_tasks(
status=status,
assigned_to=assigned_to,
project_id=project_id,
priority=priority,
tags=tag_list,
limit=limit,
offset=offset
)
return {"items": tasks, "total": len(tasks)}
@router.post("/tasks")
async def create_task(
task: TaskCreate,
user: User = Depends(_inject_user)
):
"""Create a new task"""
created_task = await opc_db.create_task(
title=task.title,
description=task.description,
status=task.status,
priority=task.priority,
project_id=task.project_id,
assigned_to=task.assigned_to,
assigned_type=task.assigned_type,
tags=task.tags,
due_date=task.due_date,
actor=user.username
)
# Start automatic timer if status is in_progress
if task.status == "in_progress":
await opc_db.start_time_entry(
task_id=created_task["id"],
project_id=task.project_id,
actor=user.username
)
return created_task
@router.get("/tasks/{task_id}")
async def get_task(
task_id: int,
user: User = Depends(_inject_user)
):
"""Get task details"""
tasks = await opc_db.get_tasks(limit=1, offset=0)
task = next((t for t in tasks if t["id"] == task_id), None)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
@router.put("/tasks/{task_id}")
async def update_task(
task_id: int,
task: TaskUpdate,
user: User = Depends(_inject_user)
):
"""Update a task"""
updates = task.dict(exclude_unset=True)
# Get current task status
tasks = await opc_db.get_tasks(limit=1000, offset=0)
current_task = next((t for t in tasks if t["id"] == task_id), None)
if not current_task:
raise HTTPException(status_code=404, detail="Task not found")
# Handle automatic time tracking
old_status = current_task["status"]
new_status = updates.get("status", old_status)
# Start timer when moving to in_progress
if old_status != "in_progress" and new_status == "in_progress":
await opc_db.start_time_entry(
task_id=task_id,
project_id=current_task.get("project_id"),
actor=user.username
)
# Stop timer when moving away from in_progress
if old_status == "in_progress" and new_status != "in_progress":
await opc_db.stop_time_entry(task_id=task_id)
# Set completed_at when moving to done
if new_status == "done" and old_status != "done":
updates["completed_at"] = datetime.utcnow()
updated_task = await opc_db.update_task(
task_id=task_id,
updates=updates,
actor=user.username
)
return updated_task
@router.delete("/tasks/{task_id}")
async def delete_task(
task_id: int,
user: User = Depends(_inject_user)
):
"""Delete a task"""
await opc_db.delete_task(task_id=task_id, actor=user.username)
return {"ok": True}
@router.put("/tasks/{task_id}/move")
async def move_task(
task_id: int,
move: TaskMove,
user: User = Depends(_inject_user)
):
"""Move task to a different column"""
updates = {"status": move.status}
# Get current task
tasks = await opc_db.get_tasks(limit=1000, offset=0)
current_task = next((t for t in tasks if t["id"] == task_id), None)
if not current_task:
raise HTTPException(status_code=404, detail="Task not found")
old_status = current_task["status"]
# Handle automatic time tracking
if old_status != "in_progress" and move.status == "in_progress":
await opc_db.start_time_entry(
task_id=task_id,
project_id=current_task.get("project_id"),
actor=user.username
)
if old_status == "in_progress" and move.status != "in_progress":
await opc_db.stop_time_entry(task_id=task_id)
if move.status == "done":
updates["completed_at"] = datetime.utcnow()
updated_task = await opc_db.update_task(
task_id=task_id,
updates=updates,
actor=user.username
)
return updated_task
@router.post("/tasks/{task_id}/assign")
async def assign_task(
task_id: int,
assign: TaskAssign,
user: User = Depends(_inject_user)
):
"""Assign task to human or agent"""
updates = {
"assigned_to": assign.assigned_to,
"assigned_type": assign.assigned_type
}
updated_task = await opc_db.update_task(
task_id=task_id,
updates=updates,
actor=user.username
)
# If assigning to agent, create execution record
if assign.assigned_type == "agent":
await opc_db.create_agent_execution(
task_id=task_id,
agent_id=assign.assigned_to,
actor=user.username
)
return updated_task
@router.get("/tasks/{task_id}/history")
async def get_task_history(
task_id: int,
user: User = Depends(_inject_user)
):
"""Get task history"""
history = await opc_db.get_task_history(task_id=task_id)
return {"items": history}
@router.get("/tasks/{task_id}/time")
async def get_task_time(
task_id: int,
user: User = Depends(_inject_user)
):
"""Get time entries for a task"""
entries = await opc_db.get_time_entries(task_id=task_id)
total_minutes = sum(e["duration_minutes"] for e in entries)
return {
"entries": entries,
"total_minutes": total_minutes,
"total_hours": round(total_minutes / 60, 2)
}