Upload app/services/dashboard_service.py

This commit is contained in:
2026-06-04 00:06:26 +00:00
committed by leocrm-bot
parent 26fe49490d
commit 8d904c316b
+66
View File
@@ -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"]