from typing import Any from sqlalchemy import func, select from sqlalchemy.orm import Session 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 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)) 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, ) 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, "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, "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)