From 15547aa38aa47c38d35eaf25b75919725e7676bc Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Thu, 4 Jun 2026 00:06:26 +0000 Subject: [PATCH] Upload app/services/dashboard_service.py --- app/services/dashboard_service.py | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 app/services/dashboard_service.py diff --git a/app/services/dashboard_service.py b/app/services/dashboard_service.py new file mode 100644 index 0000000..6ffb7b2 --- /dev/null +++ b/app/services/dashboard_service.py @@ -0,0 +1,66 @@ +"""Dashboard service: KPIs and activity feed.""" + +from __future__ import annotations + +from datetime import UTC, datetime +from decimal import Decimal + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.activity import Activity +from app.models.deal import Deal, DealStage +from app.services._base import OrgScopedQuery + + +async def get_kpis(db: AsyncSession, *, org_id: int) -> dict[str, object]: + """Compute headline KPIs for the org's dashboard.""" + scoped = OrgScopedQuery(Deal, db, org_id=org_id) + open_stages = (DealStage.lead, DealStage.qualified, DealStage.proposal, DealStage.negotiation) + + all_deals = await scoped.list(skip=0, limit=10_000, order_by=Deal.id) + open_deals = [d for d in all_deals if d.stage in open_stages] + open_deals_count = len(open_deals) + + pipeline_value = float( + sum((d.value for d in open_deals), Decimal("0")) + ) + + now = datetime.now(UTC) + # SQLite returns naive datetimes; strip tz for comparison + month_start = now.replace(tzinfo=None, day=1, hour=0, minute=0, second=0, microsecond=0) + won_this_month = [ + d for d in all_deals + if d.stage == DealStage.won and d.created_at >= month_start + ] + won_count = len(won_this_month) + + won_total = sum(1 for d in all_deals if d.stage == DealStage.won) + lost_total = sum(1 for d in all_deals if d.stage == DealStage.lost) + if won_total + lost_total == 0: + conversion_rate = 0.0 + else: + conversion_rate = round((won_total / (won_total + lost_total)) * 100, 2) + + return { + "open_deals_count": open_deals_count, + "pipeline_value": pipeline_value, + "won_this_month": won_count, + "conversion_rate": conversion_rate, + } + + +async def get_activity_feed( + db: AsyncSession, *, org_id: int, limit: int = 20 +) -> list[Activity]: + """Return the most recent activities, ordered by created_at desc.""" + result = await db.execute( + select(Activity) + .where(Activity.org_id == org_id, Activity.deleted_at.is_(None)) + .order_by(Activity.created_at.desc()) + .limit(limit) + ) + return list(result.scalars().all()) + + +__all__ = ["get_activity_feed", "get_kpis"]