PMFlow/backend/app/api/sstc_api/tasks_board_status.py

127 lines
4.5 KiB
Python

from __future__ import annotations
from .shared import *
from ...services.sstc.entity_lookup import _ensure_project, _ensure_task
from ...services.sstc.governance import _is_manager
from ...services.sstc.signal_core import _parse_node_id, _parse_project_id, _sync_pmflow_rag_async
router = APIRouter(tags=["sstc"])
@router.get("/projects/{project_id}/tasks/board")
async def get_task_board(
project_id: str,
assignee_id: Optional[int] = Query(default=None),
priority: Optional[str] = Query(default=None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
pid = _parse_project_id(project_id)
tenant_id = int(current_user.tenant_id)
await _ensure_project(SSTCService(db, tenant_id=tenant_id), pid)
result = await db.execute(
select(Task).where(
and_(
Task.project_id == pid,
Task.tenant_id == tenant_id,
)
)
)
tasks = list(result.scalars().all())
if assignee_id is not None:
tasks = [t for t in tasks if t.assignee_id == assignee_id]
if priority:
tasks = [t for t in tasks if (t.priority or "").lower() == priority.lower()]
rows = []
for t in tasks:
risk_count = 0
if (t.status or "").lower() == "blocked":
risk_count += 1
if t.due_date and t.due_date < now_cn_aware().replace(tzinfo=None) and (t.status or "").lower() not in DONE_STATUSES:
risk_count += 1
rows.append(
{
"id": str(t.id),
"title": t.title,
"description": t.description,
"status": t.status,
"priority": t.priority,
"assignee_id": str(t.assignee_id) if t.assignee_id else None,
"due_date": t.due_date.isoformat() if t.due_date else None,
"estimated_hours": float(t.estimated_hours) if t.estimated_hours else None,
"risk_count": risk_count,
"truth_node_id": str(t.truth_node_id),
}
)
return success_response(rows)
@router.patch("/projects/{project_id}/tasks/{task_id}/status")
async def patch_task_status(
project_id: str,
task_id: str,
body: TaskStatusPatchRequest,
background_tasks: BackgroundTasks,
permission_user: User = Depends(require_project_member_or_manager),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
_ = permission_user
pid = _parse_project_id(project_id)
tid = _parse_node_id(task_id, "task_id")
tenant_id = int(current_user.tenant_id)
await _ensure_project(SSTCService(db, tenant_id=tenant_id), pid)
task = await _ensure_task(db, tenant_id=tenant_id, project_id=pid, task_id=tid)
if not _is_manager(current_user) and int(task.assignee_id or 0) != int(current_user.id):
raise HTTPException(status_code=403, detail="仅负责人或任务执行人可更新状态")
before_status = (task.status or "").lower()
before = {"status": task.status}
task.status = body.status
if body.status in DONE_STATUSES:
task.completed_at = now_cn_naive()
await log_operation(
db,
tenant_id=tenant_id,
user_id=int(current_user.id),
project_id=pid,
entity_type="task",
entity_id=str(tid),
action="status_change",
before_data=before,
after_data={"status": body.status},
)
if task.assignee_id:
await workload_service.refresh_user_assigned_hours_from_tasks(
db,
tenant_id=tenant_id,
user_id=int(task.assignee_id),
)
else:
await db.commit()
await context_cache_service.invalidate_system_context(tenant_id=tenant_id)
if task.assignee_id:
await context_cache_service.invalidate_user_context(
tenant_id=tenant_id,
user_id=int(task.assignee_id),
)
became_done = (body.status or "").lower() in DONE_STATUSES and before_status not in DONE_STATUSES
if became_done and task.assignee_id:
background_tasks.add_task(
skill_score_service.process_task_completion,
task_id=int(task.id),
tenant_id=tenant_id,
operator_user_id=int(current_user.id),
)
_sync_pmflow_rag_async(
tenant_id=tenant_id,
record_type="task",
record_id=int(task.id),
project_id=pid,
title=task.title,
content=(task.description or "") + f"\n状态: {task.status}",
)
return success_response({"id": str(task.id), "status": task.status})