from __future__ import annotations from datetime import datetime from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse, RedirectResponse from routes.template_filters import create_templates from dependencies import get_current_user, get_unit_of_work from models import ScenarioStatus, User from services.unit_of_work import UnitOfWork router = APIRouter(tags=["Dashboard"]) templates = create_templates() def _format_timestamp(moment: datetime | None) -> str | None: if moment is None: return None return moment.strftime("%Y-%m-%d") def _format_timestamp_with_time(moment: datetime | None) -> str | None: if moment is None: return None return moment.strftime("%Y-%m-%d %H:%M") def _load_metrics(uow: UnitOfWork) -> dict[str, object]: if not uow.projects or not uow.scenarios or not uow.financial_inputs: raise RuntimeError("UnitOfWork repositories not initialised") total_projects = uow.projects.count() active_scenarios = uow.scenarios.count_by_status(ScenarioStatus.ACTIVE) pending_simulations = uow.scenarios.count_by_status(ScenarioStatus.DRAFT) last_import_at = uow.financial_inputs.latest_created_at() return { "total_projects": total_projects, "active_scenarios": active_scenarios, "pending_simulations": pending_simulations, "last_import": _format_timestamp(last_import_at), } def _load_recent_projects(uow: UnitOfWork) -> list: if not uow.projects: raise RuntimeError("Project repository not initialised") return list(uow.projects.recent(limit=5)) def _load_simulation_updates(uow: UnitOfWork) -> list[dict[str, object]]: updates: list[dict[str, object]] = [] if not uow.scenarios: raise RuntimeError("Scenario repository not initialised") scenarios = uow.scenarios.recent(limit=5, with_project=True) for scenario in scenarios: project_name = scenario.project.name if scenario.project else f"Project #{scenario.project_id}" timestamp_label = _format_timestamp_with_time(scenario.updated_at) updates.append( { "title": f"{scenario.name} ยท {scenario.status.value.title()}", "description": f"Latest update recorded for {project_name}.", "timestamp": scenario.updated_at, "timestamp_label": timestamp_label, } ) return updates def _load_scenario_alerts( request: Request, uow: UnitOfWork ) -> list[dict[str, object]]: alerts: list[dict[str, object]] = [] if not uow.scenarios: raise RuntimeError("Scenario repository not initialised") drafts = uow.scenarios.list_by_status( ScenarioStatus.DRAFT, limit=3, with_project=True ) for scenario in drafts: project_name = scenario.project.name if scenario.project else f"Project #{scenario.project_id}" alerts.append( { "title": f"Draft scenario: {scenario.name}", "message": f"{project_name} has a scenario awaiting validation.", "link": request.url_for( "projects.view_project", project_id=scenario.project_id ), } ) if not alerts: archived = uow.scenarios.list_by_status( ScenarioStatus.ARCHIVED, limit=3, with_project=True ) for scenario in archived: project_name = scenario.project.name if scenario.project else f"Project #{scenario.project_id}" alerts.append( { "title": f"Archived scenario: {scenario.name}", "message": f"Review archived scenario insights for {project_name}.", "link": request.url_for( "scenarios.view_scenario", scenario_id=scenario.id ), } ) return alerts @router.get("/", include_in_schema=False, name="dashboard.home", response_model=None) def dashboard_home( request: Request, user: User | None = Depends(get_current_user), uow: UnitOfWork = Depends(get_unit_of_work), ) -> HTMLResponse | RedirectResponse: if user is None: return RedirectResponse(request.url_for("auth.login_form"), status_code=303) context = { "metrics": _load_metrics(uow), "recent_projects": _load_recent_projects(uow), "simulation_updates": _load_simulation_updates(uow), "scenario_alerts": _load_scenario_alerts(request, uow), "export_modals": { "projects": request.url_for("exports.modal", dataset="projects"), "scenarios": request.url_for("exports.modal", dataset="scenarios"), }, } return templates.TemplateResponse(request, "dashboard.html", context)