fix: Backend added along with missing pages
Some checks failed
Velocity-OS Deployment Pipeline / lint (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / notify-ingress (push) Has been cancelled
Some checks failed
Velocity-OS Deployment Pipeline / lint (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / build-and-push (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (agents) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (core) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (media-engine) (push) Has been cancelled
Velocity-OS Deployment Pipeline / sign-images (webos) (push) Has been cancelled
Velocity-OS Deployment Pipeline / notify-ingress (push) Has been cancelled
This commit is contained in:
114
core/api/api/routes_dashboard.py
Normal file
114
core/api/api/routes_dashboard.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from backend.auth.dependencies import UserPrincipal, get_current_user
|
||||
from backend.crm.canonical_schema import ensure_canonical_crm_schema
|
||||
|
||||
router = APIRouter(dependencies=[Depends(get_current_user)])
|
||||
|
||||
|
||||
async def _pool(request: Request):
|
||||
await ensure_canonical_crm_schema(request.app)
|
||||
pool = getattr(request.app.state, "db_pool", None)
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=503, detail="Database unavailable.")
|
||||
return pool
|
||||
|
||||
|
||||
def _money_inr(value: Any) -> str:
|
||||
amount = float(value or 0)
|
||||
if amount >= 10_000_000:
|
||||
return f"₹{amount / 10_000_000:.1f}Cr"
|
||||
if amount >= 100_000:
|
||||
return f"₹{amount / 100_000:.1f}L"
|
||||
return f"₹{amount:,.0f}"
|
||||
|
||||
|
||||
@router.get("/dashboard/morning-brief")
|
||||
async def morning_brief(request: Request, user: UserPrincipal = Depends(get_current_user)) -> dict[str, Any]:
|
||||
"""Command pillar briefing derived from canonical CRM data."""
|
||||
pool = await _pool(request)
|
||||
async with pool.acquire() as conn:
|
||||
people_count = await conn.fetchval("SELECT COUNT(*) FROM crm_people")
|
||||
active_leads = await conn.fetchval(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM crm_leads
|
||||
WHERE status NOT IN ('booked', 'lost', 'dormant')
|
||||
"""
|
||||
)
|
||||
open_pipeline = await conn.fetchval(
|
||||
"""
|
||||
SELECT COALESCE(SUM(value), 0)
|
||||
FROM crm_opportunities
|
||||
WHERE stage NOT IN ('closed_won', 'closed_lost')
|
||||
"""
|
||||
)
|
||||
avg_qd = await conn.fetchval("SELECT COALESCE(AVG(current_value), 0) FROM intel_qd_scores")
|
||||
overdue_tasks = await conn.fetchval(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM intel_reminders
|
||||
WHERE status IN ('pending', 'confirmed') AND due_at < NOW()
|
||||
"""
|
||||
)
|
||||
stages = await conn.fetch(
|
||||
"""
|
||||
SELECT status::text AS id, INITCAP(REPLACE(status::text, '_', ' ')) AS label, COUNT(*)::int AS count
|
||||
FROM crm_leads
|
||||
GROUP BY status
|
||||
ORDER BY MIN(created_at) NULLS LAST, status::text
|
||||
"""
|
||||
)
|
||||
actions = await conn.fetch(
|
||||
"""
|
||||
SELECT p.person_id::text, p.full_name, n.recommended_action, n.priority, n.rationale,
|
||||
COALESCE(q.current_value, 0)::float AS qd_score
|
||||
FROM read_next_best_action n
|
||||
JOIN crm_people p ON p.person_id = n.person_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT current_value
|
||||
FROM intel_qd_scores
|
||||
WHERE person_id = p.person_id
|
||||
ORDER BY current_value DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) q ON TRUE
|
||||
ORDER BY
|
||||
CASE n.priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END,
|
||||
qd_score DESC,
|
||||
n.computed_at DESC NULLS LAST
|
||||
LIMIT 3
|
||||
"""
|
||||
)
|
||||
|
||||
priority_cards = []
|
||||
for row in actions:
|
||||
urgency = (row["priority"] or "medium").lower()
|
||||
if urgency not in {"high", "medium", "low"}:
|
||||
urgency = "high" if urgency == "critical" else "medium"
|
||||
priority_cards.append(
|
||||
{
|
||||
"id": row["person_id"],
|
||||
"type": "follow_up",
|
||||
"headline": row["recommended_action"] or "Follow up with client",
|
||||
"sublabel": row["rationale"],
|
||||
"personId": row["person_id"],
|
||||
"personName": row["full_name"],
|
||||
"cta": "Open client",
|
||||
"urgency": urgency,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"kpis": [
|
||||
{"label": "Clients", "value": str(int(people_count or 0)), "sublabel": "canonical CRM records"},
|
||||
{"label": "Active leads", "value": str(int(active_leads or 0)), "sublabel": "not booked/lost/dormant"},
|
||||
{"label": "Open pipeline", "value": _money_inr(open_pipeline), "sublabel": f"avg QD {float(avg_qd or 0):.0f}"},
|
||||
{"label": "Overdue follow-ups", "value": str(int(overdue_tasks or 0)), "deltaPositive": False},
|
||||
],
|
||||
"priorityCards": priority_cards,
|
||||
"pipelineStages": [dict(row) for row in stages],
|
||||
}
|
||||
Reference in New Issue
Block a user