"""Executive dashboard and landing routes for the ClientFlow workbench.""" from __future__ import annotations from fastapi import APIRouter import app.admin_dashboard as legacy from app.admin_dashboard import * # noqa: F401,F403 from app.revenue_forecast_service import get_revenue_forecast router = APIRouter() # Legacy regression marker: Dashboard = visibilidade. def _stage_map(forecast: dict) -> dict[str, dict]: return {str(row.get("stage") or "").upper(): row for row in forecast.get("stage_summary", [])} @router.get("/", response_class=HTMLResponse) async def admin_home(): """Executive visibility: target, funnel health, blockers and priorities.""" metrics = get_admin_dashboard_metrics() ops = get_operations_summary(limit=10) counts = ops.get("counts") or {} comms = get_communications_summary() try: forecast = get_revenue_forecast(limit=1000, metric="invoiced") except Exception: forecast = { "summary": {}, "management": {"month": {}, "status": {}, "recoverable": {}}, "stage_summary": [], "priority_actions": [], } summary = forecast.get("summary") or {} management = forecast.get("management") or {} month = management.get("month") or {} target_amount = float(management.get("target_amount") or 0) status = management.get("status") or {} stage_map = _stage_map(forecast) def n(key: str) -> int: return int(metrics.get(key) or counts.get(key) or 0) waiting = stage_map.get("WAITING_PAYMENT", {}) ready_count = sum(int((stage_map.get(stage) or {}).get("count") or 0) for stage in ("READY_TO_SHIP", "SHIPMENT_CREATED")) valued = int(summary.get("valued_opportunities") or 0) unvalued = int(summary.get("unvalued_opportunities") or 0) dashboard_cards = [] if target_amount > 0: dashboard_cards.extend([ ("Meta mensal", money_html(target_amount), "/forecast", management.get("metric_label") or "Faturação emitida"), ("Realizado", money_html(month.get("realised") or 0), "/forecast", f"{int(summary.get('realised_count') or 0)} registo(s) no mês"), ("Previsão base", money_html(month.get("forecast_total") or 0), "/forecast", str(status.get("label") or "Sem classificação")), ("Desvio", money_html(management.get("gap") or 0), "/forecast", "Falta para a meta" if management.get("gap") else "Meta suportada"), ]) dashboard_cards.extend([ ("Oportunidades abertas", counts.get("open_opportunities", 0), "/opportunities?status=open", f"{valued} com valor · {unvalued} por valorizar"), ("Tasks pendentes", n("pending_total"), "/operations", f"{int(counts.get('overdue_tasks') or 0)} atrasada(s)"), ("A aguardar pagamento", int(waiting.get("count") or 0), "/operations?scope=financeiro", f"Valor conhecido {money_html(waiting.get('gross') or 0)}"), ("Prontos/envio criado", ready_count, "/operations?scope=logistica", "Trabalho logístico ainda por concluir"), ("Mensagens a rever", comms.get("open", 0), "/operations", "Ações vindas do Chatwoot"), ("Erros de integração", counts.get("outbox_failed", 0), "/outbox?status=failed", "Jasmin / Packlink / outbox"), ("Clientes incompletos", counts.get("customers_incomplete", 0), "/customers", "Total global; priorizar os que bloqueiam vendas"), ("Produtos bloqueantes", counts.get("products_missing_jasmin", 0), "/products?active=missing_jasmin", "Ativos sem Artigo Jasmin"), ]) cards_html = "".join(kpi_card(label, value, href, hint) for label, value, href, hint in dashboard_cards) alert_items = [] if unvalued: severity = "cf-chip-red" if (summary.get("value_coverage") or 0) < 0.5 else "cf-chip-orange" alert_items.append(("Funil sem valor", f"{unvalued} oportunidade(s) sem valor comercial", "/forecast", severity)) if int(waiting.get("count") or 0): alert_items.append(("Pagamentos a acelerar", f"{int(waiting.get('count') or 0)} oportunidade(s) · {money_html(waiting.get('gross') or 0)}", "/operations?scope=financeiro", "cf-chip-orange")) if ready_count: alert_items.append(("Logística pendente", f"{ready_count} processo(s) pronto(s) ou com envio criado", "/operations?scope=logistica", "cf-chip-orange")) if int(counts.get("outbox_failed") or 0): alert_items.append(("Erro de integração", f"{counts.get('outbox_failed')} ação(ões) falhadas", "/outbox?status=failed", "cf-chip-red")) if int(counts.get("products_missing_jasmin") or 0): alert_items.append(("Produto bloqueante", f"{counts.get('products_missing_jasmin')} produto(s) sem Artigo Jasmin", "/products?active=missing_jasmin", "cf-chip-red")) alert_html = "".join( f'''
{esc(title)}
{detail}
Abrir →
''' for title, detail, href, chip in alert_items[:5] ) or '
Sem alertas críticos neste momento.
' action_rows = "" for action in (forecast.get("priority_actions") or [])[:5]: action_rows += f''' {esc(action.get('customer_name') or action.get('title') or 'Oportunidade')} {esc(action.get('recommended_action') or 'Abrir e rever')} {money_html(action.get('impact_amount') or action.get('amount') or 0)} ''' if not action_rows: action_rows = 'Sem ações comerciais prioritárias calculadas.' body = f"""
Dashboard executivo.Mostra desempenho, saúde do funil, bloqueios e prioridades. O Centro de trabalho continua a organizar a execução diária.
Metas e previsãoAbrir Centro de trabalho
{cards_html}

Alertas

Sinais globais com impacto comercial ou operacional.
Resolver
{alert_html}

Ações de maior impacto

Prioridades calculadas a partir do funil valorizado.
{action_rows}
ClienteAçãoImpacto

Modelo operacional v4.7

Dashboard
Estado, desempenho e riscos.
Centro de trabalho
Próximo trabalho humano.
Metas e previsão
Meta, desvio, recuperação e pipeline.
Oportunidade
Contexto, documentos, tasks e timeline.
""" return layout("Dashboard", "Visão executiva do negócio e do sistema", body, "overview")