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 Project, RiskEvent, WorkReport, WorkTask from app.modules.business.service import serialize_model from app.modules.risk.service import RiskService 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) latest_reports = self.db.execute( select(WorkReport).order_by(WorkReport.id.desc()).limit(5) ).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, "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_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)