feat: initial commit
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
"""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],
|
||||
)
|
||||
Reference in New Issue
Block a user