"""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'''
| Cliente | Ação | Impacto |
|---|