Independent FastAPI backend for Maskan CRM. Owns contacts, organizations, leads, pipelines, activities, products, quotes, users, permissions, audit records and first-party integration credentials, with Alembic migrations against PostgreSQL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
from datetime import UTC, datetime
|
|
from decimal import Decimal
|
|
|
|
from fastapi import APIRouter
|
|
from sqlalchemy import func, select
|
|
|
|
from app.core.security import CurrentUser, Database
|
|
from app.models import Activity, Contact, Lead, LeadSource, Stage
|
|
from app.schemas import (
|
|
DashboardResponse,
|
|
DashboardSourceMetric,
|
|
DashboardStageMetric,
|
|
)
|
|
from app.services import activity_load_options, activity_to_out, decimal_or_zero
|
|
|
|
router = APIRouter(tags=["Dashboard"])
|
|
|
|
|
|
@router.get("/dashboard", response_model=DashboardResponse)
|
|
def get_dashboard(user: CurrentUser, db: Database) -> DashboardResponse:
|
|
tenant_filter = Lead.tenant_id == user.tenant_id
|
|
total_revenue = db.scalar(
|
|
select(func.sum(Lead.value)).where(tenant_filter, Lead.status == "won"),
|
|
)
|
|
open_pipeline_value = db.scalar(
|
|
select(func.sum(Lead.value)).where(tenant_filter, Lead.status == "open"),
|
|
)
|
|
total_leads = db.scalar(select(func.count(Lead.id)).where(tenant_filter)) or 0
|
|
open_leads = db.scalar(
|
|
select(func.count(Lead.id)).where(tenant_filter, Lead.status == "open"),
|
|
) or 0
|
|
won_leads = db.scalar(
|
|
select(func.count(Lead.id)).where(tenant_filter, Lead.status == "won"),
|
|
) or 0
|
|
lost_leads = db.scalar(
|
|
select(func.count(Lead.id)).where(tenant_filter, Lead.status == "lost"),
|
|
) or 0
|
|
total_contacts = db.scalar(
|
|
select(func.count(Contact.id)).where(Contact.tenant_id == user.tenant_id),
|
|
) or 0
|
|
overdue_activities = db.scalar(
|
|
select(func.count(Activity.id)).where(
|
|
Activity.tenant_id == user.tenant_id,
|
|
Activity.is_done.is_(False),
|
|
Activity.due_at < datetime.now(UTC),
|
|
),
|
|
) or 0
|
|
|
|
stage_rows = db.execute(
|
|
select(
|
|
Stage.id,
|
|
Stage.name,
|
|
Stage.color,
|
|
func.count(Lead.id),
|
|
func.coalesce(func.sum(Lead.value), 0),
|
|
)
|
|
.outerjoin(
|
|
Lead,
|
|
(Lead.stage_id == Stage.id) & (Lead.tenant_id == user.tenant_id),
|
|
)
|
|
.where(Stage.tenant_id == user.tenant_id)
|
|
.group_by(Stage.id, Stage.name, Stage.color, Stage.position)
|
|
.order_by(Stage.position),
|
|
).all()
|
|
|
|
source_rows = db.execute(
|
|
select(
|
|
func.coalesce(LeadSource.name, "Direct"),
|
|
func.count(Lead.id),
|
|
)
|
|
.select_from(Lead)
|
|
.outerjoin(LeadSource, Lead.source_id == LeadSource.id)
|
|
.where(Lead.tenant_id == user.tenant_id)
|
|
.group_by(LeadSource.name)
|
|
.order_by(func.count(Lead.id).desc()),
|
|
).all()
|
|
|
|
recent = db.scalars(
|
|
select(Activity)
|
|
.where(Activity.tenant_id == user.tenant_id)
|
|
.options(*activity_load_options())
|
|
.order_by(Activity.created_at.desc())
|
|
.limit(5),
|
|
).all()
|
|
|
|
return DashboardResponse(
|
|
total_revenue=decimal_or_zero(total_revenue),
|
|
open_pipeline_value=decimal_or_zero(open_pipeline_value),
|
|
total_leads=total_leads,
|
|
open_leads=open_leads,
|
|
won_leads=won_leads,
|
|
lost_leads=lost_leads,
|
|
total_contacts=total_contacts,
|
|
overdue_activities=overdue_activities,
|
|
stages=[
|
|
DashboardStageMetric(
|
|
stage_id=row[0],
|
|
stage_name=row[1],
|
|
color=row[2],
|
|
lead_count=row[3],
|
|
total_value=Decimal(row[4]),
|
|
)
|
|
for row in stage_rows
|
|
],
|
|
sources=[
|
|
DashboardSourceMetric(source=row[0], lead_count=row[1])
|
|
for row in source_rows
|
|
],
|
|
recent_activities=[activity_to_out(item) for item in recent],
|
|
)
|
|
|