from __future__ import annotations import logging from datetime import datetime, timezone from typing import Any from fastapi import Request from fastapi.templating import Jinja2Templates from services.navigation import NavigationService from services.session import AuthSession from services.unit_of_work import UnitOfWork logger = logging.getLogger(__name__) def format_datetime(value: Any) -> str: """Render datetime values consistently for templates.""" if not isinstance(value, datetime): return "" if value.tzinfo is None: value = value.replace(tzinfo=timezone.utc) return value.strftime("%Y-%m-%d %H:%M UTC") def currency_display(value: Any, currency_code: str | None) -> str: """Format numeric values with currency context.""" if value is None: return "—" if isinstance(value, (int, float)): formatted_value = f"{value:,.2f}" else: formatted_value = str(value) if currency_code: return f"{currency_code} {formatted_value}" return formatted_value def format_metric(value: Any, metric_name: str, currency_code: str | None = None) -> str: """Format metrics according to their semantic type.""" if value is None: return "—" currency_metrics = { "npv", "inflows", "outflows", "net", "total_inflows", "total_outflows", "total_net", } if metric_name in currency_metrics and currency_code: return currency_display(value, currency_code) percentage_metrics = {"irr", "payback_period"} if metric_name in percentage_metrics: if isinstance(value, (int, float)): return f"{value:.2f}%" return f"{value}%" if isinstance(value, (int, float)): return f"{value:,.2f}" return str(value) def percentage_display(value: Any) -> str: """Format numeric values as percentages.""" if value is None: return "—" if isinstance(value, (int, float)): return f"{value:.2f}%" return f"{value}%" def period_display(value: Any) -> str: """Format period values in years.""" if value is None: return "—" if isinstance(value, (int, float)): if value == int(value): return f"{int(value)} years" return f"{value:.1f} years" return str(value) def register_common_filters(templates: Jinja2Templates) -> None: templates.env.filters["format_datetime"] = format_datetime templates.env.filters["currency_display"] = currency_display templates.env.filters["format_metric"] = format_metric templates.env.filters["percentage_display"] = percentage_display templates.env.filters["period_display"] = period_display def _sidebar_navigation_for_request(request: Request | None): if request is None: return None cached = getattr(request.state, "_navigation_sidebar_dto", None) if cached is not None: return cached session_context = getattr(request.state, "auth_session", None) if isinstance(session_context, AuthSession): session = session_context else: session = AuthSession.anonymous() try: with UnitOfWork() as uow: if not uow.navigation: logger.debug("Navigation repository unavailable for sidebar rendering") sidebar_dto = None else: service = NavigationService(uow.navigation) sidebar_dto = service.build_sidebar(session=session, request=request) except Exception: # pragma: no cover - defensive fallback for templates logger.exception("Failed to build sidebar navigation during template render") sidebar_dto = None setattr(request.state, "_navigation_sidebar_dto", sidebar_dto) return sidebar_dto def register_navigation_globals(templates: Jinja2Templates) -> None: templates.env.globals["get_sidebar_navigation"] = _sidebar_navigation_for_request def create_templates() -> Jinja2Templates: templates = Jinja2Templates(directory="templates") register_common_filters(templates) register_navigation_globals(templates) return templates __all__ = [ "format_datetime", "currency_display", "format_metric", "percentage_display", "period_display", "register_common_filters", "register_navigation_globals", "create_templates", ]