134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
|
|
from ...services.context_cache_service import context_cache_service
|
|
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),
|
|
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)
|
|
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),
|
|
"required_skill_tag_ids": list(t.required_skill_tag_ids or []),
|
|
"ai_recommended_assignees": t.ai_recommended_assignees,
|
|
"ai_breakdown_payload": t.ai_breakdown_payload,
|
|
"manual_overrides": t.manual_overrides,
|
|
"auto_assigned": bool(t.assignee_id) and not bool((t.manual_overrides or {}).get("assignee_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})
|