"""Dashboard summary endpoint. Returns only the aggregates the homepage actually renders — counts, top items, totals, and a trend-chart series. Replaces a Dashboard load that previously fetched five full collections (raw materials, mixes, all product cost breakdowns, scenarios, data-quality) and only used summaries from each. """ from __future__ import annotations from fastapi import APIRouter, Depends from sqlalchemy import select from sqlalchemy.orm import Session, selectinload from app.api.deps import AuthSession, require_client_session from app.db.session import get_db from app.models.mix import Mix from app.models.product import Product from app.models.raw_material import RawMaterial from app.services.client_access_service import has_access_level from app.services.costing_engine import ( calculate_mix_cost, calculate_product_cost, get_active_price, calculate_raw_material_cost, ) router = APIRouter(prefix="/api/dashboard", tags=["dashboard"]) def _can(session: AuthSession, module_key: str) -> bool: permissions = session.module_permissions or {} return has_access_level(permissions.get(module_key), "view") @router.get("/summary") def dashboard_summary( session: AuthSession = Depends(require_client_session), db: Session = Depends(get_db), ): raw_materials_summary: dict | None = None mixes_summary: dict | None = None products_summary: dict | None = None raw_series: list[float] = [] mix_series: list[float] = [] product_series: list[float] = [] if _can(session, "raw_materials") or _can(session, "dashboard"): materials = db.scalars( select(RawMaterial) .where(RawMaterial.tenant_id == session.tenant_id) .options(selectinload(RawMaterial.price_versions)) ).all() total_market_value = 0.0 latest = None latest_date = None for material in materials: active = get_active_price(material) if active is None: continue comp = calculate_raw_material_cost(material, active) raw_series.append(comp.cost_per_kg) total_market_value += active.market_value if latest_date is None or active.effective_date > latest_date: latest_date = active.effective_date latest = { "id": material.id, "name": material.name, "market_value": active.market_value, "cost_per_kg": comp.cost_per_kg, "effective_date": active.effective_date.isoformat() if active.effective_date else None, } raw_materials_summary = { "count": len(materials), "total_market_value": round(total_market_value, 4), "latest": latest, } if _can(session, "mix_master") or _can(session, "dashboard"): mix_rows = db.scalars( select(Mix).where(Mix.tenant_id == session.tenant_id).order_by(Mix.name) ).all() cost_sum = 0.0 cost_count = 0 top_mix: dict | None = None for mix in mix_rows: result = calculate_mix_cost(db, mix.id) cost_per_kg = result.get("mix_cost_per_kg") if cost_per_kg is not None: mix_series.append(cost_per_kg) cost_sum += cost_per_kg cost_count += 1 if top_mix is None or (cost_per_kg or 0) > (top_mix.get("mix_cost_per_kg") or 0): top_mix = { "id": result["id"], "name": result["name"], "client_name": result["client_name"], "ingredients_count": len(result["ingredients"]), "total_mix_kg": result["total_mix_kg"], "total_mix_cost": result["total_mix_cost"], "mix_cost_per_kg": cost_per_kg, "warnings": result["warnings"], } mixes_summary = { "count": len(mix_rows), "average_cost_per_kg": round(cost_sum / cost_count, 4) if cost_count else 0.0, "top": top_mix, } if _can(session, "products") or _can(session, "dashboard"): products = db.scalars( select(Product).where(Product.tenant_id == session.tenant_id) ).all() rows: list[dict] = [] for product in products: result = calculate_product_cost(db, product.id) finished = result.get("finished_product_delivered") or 0.0 product_series.append(finished) rows.append( { "id": product.id, "product_name": result["product_name"], "client_name": result["client_name"], "finished_product_delivered": finished, "warnings": result["warnings"], } ) rows.sort(key=lambda row: row["finished_product_delivered"], reverse=True) products_summary = { "count": len(products), "top": rows[0] if rows else None, "top_products": rows[:4], } return { "raw_materials": raw_materials_summary, "mixes": mixes_summary, "products": products_summary, # Pre-computed numeric series for the homepage trend chart so the # client doesn't need full collections to draw it. "trend_seeds": { "raw_material_cost_per_kg": raw_series, "mix_cost_per_kg": mix_series, "product_finished_delivered": product_series, }, }