from typing import Any from sqlalchemy import func, select from sqlalchemy.orm import Session from app.modules.approvals.constants import ApprovalStatus from app.modules.approvals.models import ApprovalRequest from app.modules.audit.models import AuditLog from app.modules.business.constants import DONE_STATUSES, PROJECT_CLOSED_STATUSES, StatusValue from app.modules.business.models import ( LegacySyncRun, Project, ReportPushRun, RiskEvent, WorkReport, WorkTask, ) from app.modules.business.service import serialize_model from app.modules.events.constants import EventStatus from app.modules.events.models import DomainEvent from app.modules.reports.constants import ReportPushStatus from app.modules.risk.service import RiskService from app.modules.workflows.constants import WorkflowStatus from app.modules.workflows.models import WorkflowInstance from app.modules.writebacks.constants import WritebackStatus from app.modules.writebacks.models import OfficialWritebackRun class DashboardService: """Build lightweight operational dashboard data for V2.""" def __init__(self, db: Session): self.db = db self.risks = RiskService(db) def summary(self) -> dict[str, Any]: active_projects = self._count(Project, Project.status.notin_(PROJECT_CLOSED_STATUSES)) open_tasks = self._count(WorkTask, WorkTask.status.notin_(DONE_STATUSES)) pending_approvals = self._count( ApprovalRequest, ApprovalRequest.status == ApprovalStatus.PENDING, ) open_risk_events = self._count(RiskEvent, RiskEvent.status == StatusValue.OPEN) unassigned_open_risks = self._count( RiskEvent, RiskEvent.status == StatusValue.OPEN, RiskEvent.assigned_to.is_(None), ) failed_push_runs = self._count(ReportPushRun, ReportPushRun.status == ReportPushStatus.FAILED) pending_events = self._count(DomainEvent, DomainEvent.status == EventStatus.PENDING) failed_events = self._count(DomainEvent, DomainEvent.status == EventStatus.FAILED) running_workflows = self._count( WorkflowInstance, WorkflowInstance.status == WorkflowStatus.RUNNING, ) failed_workflows = self._count( WorkflowInstance, WorkflowInstance.status == WorkflowStatus.FAILED, ) disabled_writebacks = self._count( OfficialWritebackRun, OfficialWritebackRun.status == WritebackStatus.DISABLED, ) failed_writebacks = self._count( OfficialWritebackRun, OfficialWritebackRun.status == WritebackStatus.FAILED, ) latest_reports = self.db.execute( select(WorkReport).order_by(WorkReport.id.desc()).limit(5) ).scalars() latest_push_runs = self.db.execute( select(ReportPushRun).order_by(ReportPushRun.id.desc()).limit(10) ).scalars() latest_sync_runs = self.db.execute( select(LegacySyncRun).order_by(LegacySyncRun.id.desc()).limit(10) ).scalars() latest_audit_logs = self.db.execute( select(AuditLog).order_by(AuditLog.id.desc()).limit(10) ).scalars() risk_summary = self.risks.summary() return { "metrics": { "active_projects": active_projects, "open_tasks": open_tasks, "pending_approvals": pending_approvals, "open_risk_events": open_risk_events, "unassigned_open_risks": unassigned_open_risks, "failed_push_runs": failed_push_runs, "pending_events": pending_events, "failed_events": failed_events, "running_workflows": running_workflows, "failed_workflows": failed_workflows, "disabled_writebacks": disabled_writebacks, "failed_writebacks": failed_writebacks, "risk_level": risk_summary["risk_level"], "risk_score": float(risk_summary["risk_score"]), }, "risk_counts": { "overdue_tasks": len(risk_summary["overdue_tasks"]), "delayed_projects": len(risk_summary["delayed_projects"]), "over_budget_projects": len(risk_summary["over_budget_projects"]), "fund_risks": len(risk_summary["fund_risks"]), "supplier_risks": len(risk_summary["supplier_risks"]), }, "latest_reports": [serialize_model(item) for item in latest_reports], "latest_push_runs": [serialize_model(item) for item in latest_push_runs], "latest_sync_runs": [serialize_model(item) for item in latest_sync_runs], "latest_audit_logs": [serialize_model(item) for item in latest_audit_logs], } def _count(self, model: type, *conditions: Any) -> int: stmt = select(func.count()).select_from(model) for condition in conditions: stmt = stmt.where(condition) return int(self.db.execute(stmt).scalar() or 0)