"""Analytics router — KPI dashboard endpoints.""" from datetime import date, timedelta from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel from app.database import get_db from app.utils.auth import require_admin_or_pm from app.models.user import User from app.services import kpi_service router = APIRouter(prefix="/analytics", tags=["analytics"]) # ── Response models ──────────────────────────────────────────────────────────── class ThroughputPoint(BaseModel): week: str count: int completed: int class RevenuePoint(BaseModel): month: str revenue: float order_count: int class ProcessingTimeStats(BaseModel): avg_submit_to_complete_s: Optional[float] avg_submit_to_processing_s: Optional[float] p50_s: Optional[float] p95_s: Optional[float] class ItemStatusBreakdown(BaseModel): pending: int approved: int rejected: int class RenderTimeBreakdown(BaseModel): avg_stl_s: Optional[float] avg_render_s: Optional[float] avg_total_s: Optional[float] sample_count: int class TopLevelSummary(BaseModel): total_orders: int completed_orders: int total_revenue: float total_rendering_items: int class CategoryCount(BaseModel): category: str count: int class ProductCategoryStats(BaseModel): unique_products_rendered: int total_products: int products_with_cad: int products_by_category: list[CategoryCount] class OutputTypeUsagePoint(BaseModel): output_type: str count: int class RenderStatusDistribution(BaseModel): pending: int processing: int completed: int failed: int class RendererUsagePoint(BaseModel): renderer: str count: int class TopProductEntry(BaseModel): pim_id: str product_name: Optional[str] category: str order_count: int class CategoryRevenueEntry(BaseModel): category: str order_count: int revenue: float class RenderBackendStatsEntry(BaseModel): backend: str total: int completed: int failed: int avg_render_s: float | None p50_render_s: float | None class RenderTimeByOutputType(BaseModel): output_type: str job_count: int avg_render_s: float | None min_render_s: float | None max_render_s: float | None p50_render_s: float | None class OrdersByUserEntry(BaseModel): full_name: str email: str role: str order_count: int revenue: float class DashboardKPIs(BaseModel): summary: TopLevelSummary throughput: list[ThroughputPoint] revenue: list[RevenuePoint] processing_times: ProcessingTimeStats item_status: ItemStatusBreakdown render_times: RenderTimeBreakdown product_stats: ProductCategoryStats output_type_usage: list[OutputTypeUsagePoint] render_status: RenderStatusDistribution renderer_usage: list[RendererUsagePoint] top_products: list[TopProductEntry] orders_by_user: list[OrdersByUserEntry] category_revenue: list[CategoryRevenueEntry] render_backend_stats: list[RenderBackendStatsEntry] render_time_by_output_type: list[RenderTimeByOutputType] # ── Endpoints ────────────────────────────────────────────────────────────────── @router.get("/dashboard", response_model=DashboardKPIs) async def get_dashboard_kpis( date_from: date | None = Query(None, description="Start date (ISO), e.g. 2025-01-01"), date_to: date | None = Query(None, description="End date (ISO), e.g. 2025-06-30"), _user: User = Depends(require_admin_or_pm), db: AsyncSession = Depends(get_db), ) -> DashboardKPIs: """Aggregate KPI data for the analytics dashboard.""" # Default: last 12 weeks if date_to is None: date_to = date.today() if date_from is None: date_from = date_to - timedelta(weeks=12) df = date_from.isoformat() dt = date_to.isoformat() summary_data, throughput_data, revenue_data, proc_data, item_data, render_data = ( await kpi_service.top_level_summary(db, df, dt), await kpi_service.order_throughput_by_week(db, df, dt), await kpi_service.revenue_overview(db, df, dt), await kpi_service.processing_time_stats(db, df, dt), await kpi_service.item_status_breakdown(db, df, dt), await kpi_service.render_time_breakdown(db, df, dt), ) product_stats_data = await kpi_service.product_and_category_stats(db, df, dt) ot_usage_data = await kpi_service.output_type_usage(db, df, dt) render_status_data, renderer_usage_data = await kpi_service.render_status_distribution(db, df, dt) top_products_data = await kpi_service.top_products(db, df, dt) cat_revenue_data = await kpi_service.category_revenue(db, df, dt) users_data = await kpi_service.orders_by_user(db, df, dt) backend_stats_data = await kpi_service.render_backend_stats(db, df, dt) render_time_ot_data = await kpi_service.render_time_by_output_type(db, df, dt) return DashboardKPIs( summary=TopLevelSummary(**summary_data), throughput=[ThroughputPoint(**p) for p in throughput_data], revenue=[RevenuePoint(**p) for p in revenue_data], processing_times=ProcessingTimeStats(**proc_data), item_status=ItemStatusBreakdown(**item_data), render_times=RenderTimeBreakdown(**render_data), product_stats=ProductCategoryStats( **{**product_stats_data, "products_by_category": [ CategoryCount(**c) for c in product_stats_data["products_by_category"] ]} ), output_type_usage=[OutputTypeUsagePoint(**p) for p in ot_usage_data], render_status=RenderStatusDistribution(**render_status_data), renderer_usage=[RendererUsagePoint(**p) for p in renderer_usage_data], top_products=[TopProductEntry(**p) for p in top_products_data], orders_by_user=[OrdersByUserEntry(**p) for p in users_data], category_revenue=[CategoryRevenueEntry(**p) for p in cat_revenue_data], render_backend_stats=[RenderBackendStatsEntry(**p) for p in backend_stats_data], render_time_by_output_type=[RenderTimeByOutputType(**p) for p in render_time_ot_data], )