"""Program view aggregation for AI PMO cadence service.""" from __future__ import annotations from .shared import Any, Budget, Project, ProjectDomain, Task, WorkItem, and_, func, select class CadenceProgramViewMixin: """Builds cross-project domain program metrics.""" async def build_program_view(self) -> dict[str, Any]: projects = ( await self.db.execute( select(Project, ProjectDomain) .outerjoin( ProjectDomain, and_( ProjectDomain.id == Project.domain_id, ProjectDomain.tenant_id == self.tenant_id, ), ) .where( and_( Project.tenant_id == self.tenant_id, Project.status.in_(["active", "planning", "doing", "todo"]), ) ) .order_by(Project.id.asc()) ) ).all() domain_groups: dict[str, dict[str, Any]] = {} for project, domain in projects: key = str((domain.slug if domain is not None else "unscoped") or "unscoped") group = domain_groups.setdefault( key, { "program_key": key, "program_name": str((domain.name if domain is not None else "未分组项目") or "未分组项目"), "project_count": 0, "project_ids": [], "blocked_task_count": 0, "pending_manual_count": 0, "budget_usage_ratio_max": 0.0, }, ) group["project_count"] += 1 group["project_ids"].append(int(project.id)) if not domain_groups: return {"summary": {"program_count": 0, "active_project_count": 0}, "programs": []} project_ids = [pid for group in domain_groups.values() for pid in group["project_ids"]] task_rows = ( await self.db.execute( select(Task.project_id, func.count(Task.id)) .where( and_( Task.tenant_id == self.tenant_id, Task.project_id.in_(project_ids), Task.status == "blocked", ) ) .group_by(Task.project_id) ) ).all() task_map = {int(project_id): int(count or 0) for project_id, count in task_rows} work_rows = ( await self.db.execute( select(WorkItem.project_id, func.count(WorkItem.id)) .where( and_( WorkItem.tenant_id == self.tenant_id, WorkItem.project_id.in_(project_ids), WorkItem.state == "waiting_manual", ) ) .group_by(WorkItem.project_id) ) ).all() work_map = {int(project_id): int(count or 0) for project_id, count in work_rows} budget_rows = ( await self.db.execute( select(Budget.project_id, Budget.budget_amount_usd, Budget.current_usage_usd).where( and_( Budget.tenant_id == self.tenant_id, Budget.project_id.in_(project_ids), ) ) ) ).all() budget_ratio_map = { int(project_id): (float(usage or 0.0) / float(amount or 1.0)) if float(amount or 0.0) > 0 else 0.0 for project_id, amount, usage in budget_rows if project_id is not None } for group in domain_groups.values(): group["blocked_task_count"] = sum(task_map.get(pid, 0) for pid in group["project_ids"]) group["pending_manual_count"] = sum(work_map.get(pid, 0) for pid in group["project_ids"]) ratios = [budget_ratio_map.get(pid, 0.0) for pid in group["project_ids"]] group["budget_usage_ratio_max"] = round(max(ratios, default=0.0), 4) programs = sorted( domain_groups.values(), key=lambda item: ( item["pending_manual_count"], item["blocked_task_count"], item["project_count"], ), reverse=True, ) return { "summary": { "program_count": len(programs), "active_project_count": len(project_ids), "pending_manual_total": sum(item["pending_manual_count"] for item in programs), "blocked_task_total": sum(item["blocked_task_count"] for item in programs), }, "programs": programs, }