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], }