2026-06-04 00:06:26 +00:00
|
|
|
"""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)
|
|
|
|
|
|
2026-06-10 21:24:24 +00:00
|
|
|
pipeline_value = float(sum((d.value for d in open_deals), Decimal("0")))
|
2026-06-04 00:06:26 +00:00
|
|
|
|
|
|
|
|
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 = [
|
2026-06-10 21:24:24 +00:00
|
|
|
d for d in all_deals if d.stage == DealStage.won and d.created_at >= month_start
|
2026-06-04 00:06:26 +00:00
|
|
|
]
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-10 21:24:24 +00:00
|
|
|
async def get_activity_feed(db: AsyncSession, *, org_id: int, limit: int = 20) -> list[Activity]:
|
2026-06-04 00:06:26 +00:00
|
|
|
"""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"]
|