Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
@@ -1,102 +1,128 @@
|
||||
"""Dashboard and landing routes.
|
||||
"""Executive dashboard and landing routes for the ClientFlow workbench."""
|
||||
from __future__ import annotations
|
||||
|
||||
Moved from app.admin_dashboard in v4.7.2. The handlers still reuse
|
||||
legacy helpers to keep this refactor behavior-preserving.
|
||||
"""
|
||||
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():
|
||||
"""v4.5 clean Dashboard: visibility, not daily execution."""
|
||||
"""Executive visibility: target, funnel health, blockers and priorities."""
|
||||
metrics = get_admin_dashboard_metrics()
|
||||
ops = get_operations_summary(limit=6)
|
||||
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)
|
||||
|
||||
dashboard_cards = [
|
||||
("Oportunidades abertas", counts.get("open_opportunities", 0), "/opportunities?status=open", "Negócio em acompanhamento"),
|
||||
("Valor / documentos", counts.get("open_quotations", 0), "/finance", "Orçamentos abertos"),
|
||||
("Tasks pendentes", n("pending_total"), "/operations", "Trabalho humano por resolver"),
|
||||
("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"),
|
||||
("Pagamentos por confirmar", n("pending_financeiro"), "/operations", "Fila financeira"),
|
||||
("Envios pendentes", counts.get("shipments_pending", 0), "/orders", "Logística/Packlink"),
|
||||
("Clientes incompletos", counts.get("customers_incomplete", 0), "/customers", "Dados fiscais/morada"),
|
||||
]
|
||||
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)
|
||||
|
||||
cards_html = ""
|
||||
for label, value, href, hint in dashboard_cards:
|
||||
cards_html += kpi_card(label, value, href, hint)
|
||||
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 na outbox", "/outbox?status=failed", "cf-chip-red"))
|
||||
if int(comms.get("needs_review") or 0):
|
||||
alert_items.append(("Rever comunicação", f"{comms.get('needs_review')} mensagem(ns) com baixa confiança", "/operations", "cf-chip-orange"))
|
||||
if int(counts.get("customers_incomplete") or 0):
|
||||
alert_items.append(("Dados incompletos", f"{counts.get('customers_incomplete')} cliente(s) sem dados fiscais/morada completos", "/customers", "cf-chip-orange"))
|
||||
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) ativos sem Artigo Jasmin", "/products?active=missing_jasmin", "cf-chip-red"))
|
||||
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 = ""
|
||||
for title, detail, href, chip in alert_items[:5]:
|
||||
alert_html += f"""
|
||||
<a class="d-flex justify-content-between align-items-start gap-3 py-3 border-bottom text-reset" href="{esc(href)}">
|
||||
<div><span class="cf-chip {esc(chip)} mb-2">{esc(title)}</span><div class="fw-bold">{esc(detail)}</div></div>
|
||||
<span class="text-primary fw-bold">Abrir →</span>
|
||||
</a>
|
||||
"""
|
||||
if not alert_html:
|
||||
alert_html = '<div class="text-secondary py-3">Sem alertas críticos neste momento.</div>'
|
||||
alert_html = "".join(
|
||||
f'''<a class="d-flex justify-content-between align-items-start gap-3 py-3 border-bottom text-reset" href="{esc(href)}"><div><span class="cf-chip {esc(chip)} mb-2">{esc(title)}</span><div class="fw-bold">{detail}</div></div><span class="text-primary fw-bold">Abrir →</span></a>'''
|
||||
for title, detail, href, chip in alert_items[:5]
|
||||
) or '<div class="text-secondary py-3">Sem alertas críticos neste momento.</div>'
|
||||
|
||||
action_rows = ""
|
||||
for action in (forecast.get("priority_actions") or [])[:5]:
|
||||
action_rows += f'''
|
||||
<tr>
|
||||
<td><a href="/opportunities/{esc(action.get('id'))}"><strong>{esc(action.get('customer_name') or action.get('title') or 'Oportunidade')}</strong></a></td>
|
||||
<td>{esc(action.get('recommended_action') or 'Abrir e rever')}</td>
|
||||
<td class="text-end">{money_html(action.get('impact_amount') or action.get('amount') or 0)}</td>
|
||||
</tr>'''
|
||||
if not action_rows:
|
||||
action_rows = '<tr><td colspan="3" class="text-center text-secondary py-4">Sem ações comerciais prioritárias calculadas.</td></tr>'
|
||||
|
||||
body = f"""
|
||||
<section class="alert alert-primary border-0 shadow-sm d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<strong>Dashboard = visibilidade.</strong>
|
||||
<span class="ms-1">O Chatwoot é a inbox. O ClientFlow mostra o trabalho, bloqueios e próximas ações.</span>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="/operations">Abrir Centro de trabalho</a>
|
||||
<div><strong>Dashboard executivo.</strong><span class="ms-1">Mostra desempenho, saúde do funil, bloqueios e prioridades. O Centro de trabalho continua a organizar a execução diária.</span></div>
|
||||
<div class="d-flex gap-2"><a class="btn btn-outline-primary" href="/forecast">Metas e previsão</a><a class="btn btn-primary" href="/operations">Abrir Centro de trabalho</a></div>
|
||||
</section>
|
||||
|
||||
<section class="cf-kpi-grid">
|
||||
{cards_html}
|
||||
</section>
|
||||
<section class="cf-kpi-grid">{cards_html}</section>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-7">
|
||||
<section class="card cf-card h-100">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div><h2 class="cf-section-title">Alertas</h2><div class="small text-secondary">Sinais globais que merecem atenção.</div></div>
|
||||
<a class="btn btn-sm btn-outline-primary" href="/operations">Resolver no Centro de trabalho</a>
|
||||
</div>
|
||||
{alert_html}
|
||||
</div>
|
||||
</section>
|
||||
<section class="card cf-card h-100"><div class="card-body p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3"><div><h2 class="cf-section-title">Alertas</h2><div class="small text-secondary">Sinais globais com impacto comercial ou operacional.</div></div><a class="btn btn-sm btn-outline-primary" href="/operations">Resolver</a></div>
|
||||
{alert_html}
|
||||
</div></section>
|
||||
</div>
|
||||
<div class="col-xl-5">
|
||||
<section class="card cf-card h-100">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="cf-section-title mb-3">Modelo operacional v4.5</h2>
|
||||
<div class="d-grid gap-3">
|
||||
<div class="cf-soft-box"><strong>Dashboard</strong><div class="small text-secondary">Mostra o estado e gargalos.</div></div>
|
||||
<div class="cf-soft-box"><strong>Centro de trabalho</strong><div class="small text-secondary">Organiza o que precisa de ação agora.</div></div>
|
||||
<div class="cf-soft-box"><strong>Chatwoot → ClientFlow</strong><div class="small text-secondary">O Chatwoot continua a ser a inbox; o ClientFlow transforma mensagens em ações, tasks e timeline.</div></div>
|
||||
<div class="cf-soft-box"><strong>Oportunidade</strong><div class="small text-secondary">Mantém contexto, documentos, tasks, outbox e timeline.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card cf-card h-100"><div class="card-body p-0">
|
||||
<div class="p-4 border-bottom"><h2 class="cf-section-title mb-1">Ações de maior impacto</h2><div class="small text-secondary">Prioridades calculadas a partir do funil valorizado.</div></div>
|
||||
<div class="cf-table-wrap border-0 rounded-0"><table class="table cf-table mb-0"><thead><tr><th>Cliente</th><th>Ação</th><th class="text-end">Impacto</th></tr></thead><tbody>{action_rows}</tbody></table></div>
|
||||
</div></section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card cf-card mt-3"><div class="card-body p-4"><h2 class="cf-section-title mb-3">Modelo operacional v4.7</h2><div class="row g-3"><div class="col-md-3"><div class="cf-soft-box h-100"><strong>Dashboard</strong><div class="small text-secondary">Estado, desempenho e riscos.</div></div></div><div class="col-md-3"><div class="cf-soft-box h-100"><strong>Centro de trabalho</strong><div class="small text-secondary">Próximo trabalho humano.</div></div></div><div class="col-md-3"><div class="cf-soft-box h-100"><strong>Metas e previsão</strong><div class="small text-secondary">Meta, desvio, recuperação e pipeline.</div></div></div><div class="col-md-3"><div class="cf-soft-box h-100"><strong>Oportunidade</strong><div class="small text-secondary">Contexto, documentos, tasks e timeline.</div></div></div></div></div></section>
|
||||
"""
|
||||
return layout("Dashboard", "Visão geral do negócio e do sistema", body, "overview")
|
||||
|
||||
|
||||
return layout("Dashboard", "Visão executiva do negócio e do sistema", body, "overview")
|
||||
|
||||
Reference in New Issue
Block a user