@danhachuel/leadmaster 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/backend/__pycache__/main.cpython-311.pyc +0 -0
  3. package/backend/main.py +35 -0
  4. package/backend/services/__pycache__/__init__.cpython-311.pyc +0 -0
  5. package/backend/services/__pycache__/agents.cpython-311.pyc +0 -0
  6. package/backend/services/__pycache__/crm_agent.cpython-311.pyc +0 -0
  7. package/backend/services/__pycache__/extractors.cpython-311.pyc +0 -0
  8. package/backend/services/__pycache__/flags.cpython-311.pyc +0 -0
  9. package/backend/services/__pycache__/llm.cpython-311.pyc +0 -0
  10. package/backend/services/__pycache__/storage.cpython-311.pyc +0 -0
  11. package/backend/tests/__pycache__/conftest.cpython-311-pytest-8.4.2.pyc +0 -0
  12. package/backend/tests/__pycache__/test_agents_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  13. package/backend/tests/__pycache__/test_campaigns_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  14. package/backend/tests/__pycache__/test_cnpj_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  15. package/backend/tests/__pycache__/test_config.cpython-311-pytest-8.4.2.pyc +0 -0
  16. package/backend/tests/__pycache__/test_crm_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  17. package/backend/tests/__pycache__/test_funnel_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  18. package/backend/tests/__pycache__/test_instagram_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  19. package/backend/tests/__pycache__/test_launcher_protections.cpython-311-pytest-8.4.2.pyc +0 -0
  20. package/backend/tests/__pycache__/test_maps_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
  21. package/backend/tests/__pycache__/test_metrics.cpython-311-pytest-8.4.2.pyc +0 -0
  22. package/backend/tests/__pycache__/test_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
  23. package/backend/tests/__pycache__/test_webhook_routes.cpython-311-pytest-8.4.2.pyc +0 -0
  24. package/backend/tests/test_funnel_routes.py +65 -0
  25. package/frontend/dist/assets/{index-CC4fz5Tw.js → index-CX_h90mG.js} +15 -15
  26. package/frontend/dist/assets/{index-Bn20U7qZ.css → index-ViDiQGJH.css} +1 -1
  27. package/frontend/dist/index.html +2 -2
  28. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -37,6 +37,29 @@ normaliza para a forma canônica (ex.: `0.1.0`).
37
37
  falha se `process.stdin.resume();` desaparecer, se `.unref()` aparecer, se o
38
38
  ciclo de vida vinculado for removido ou se os handlers globais sumirem.
39
39
 
40
+ ## [0.1.22] — 2026-11-09
41
+
42
+ ### Adicionado
43
+
44
+ - **Funil de Prospecção (visão macro de conversão):**
45
+ - Rota `GET /api/funnel`: conta os leads por etapa do CRM (`Leads → Em
46
+ Contato → Proposta → Fechado`), calcula a taxa de conversão de cada
47
+ transição (`para/de × 100`, 1 decimal) e a queda percentual, e identifica
48
+ o **gargalo** — a etapa *origem* da maior queda (onde os leads param).
49
+ Resposta: `{etapas, conversoes, gargalo}`.
50
+ - Página `frontend/src/pages/Funil.tsx` (rota `/funil`, link **"Funil de
51
+ Prospecção"** no menu): **funil em CSS puro** com fatias de largura
52
+ decrescente (nome + quantidade por etapa), **cards de conversão** por
53
+ transição (verde ≥50%, âmbar ≥25%, vermelho abaixo), **alerta de gargalo**
54
+ em vermelho/laranja com "⚠️ Gargalo: muitos leads param em 'X'" e fatia
55
+ do gargalo destacada (von Restorff), zero filtros na tela (Lei de Hick) e
56
+ funil vazio com barra zerada + "Comece a prospectar para ver seu funil"
57
+ (Zeigarnik). Acesso aos dados via `api.funil()` no cliente padronizado.
58
+ - **4 novos testes pytest** (`backend/tests/test_funnel_routes.py`):
59
+ conversões + gargalo com distribuição [10,5,2,1] (taxas 50/40/50 → gargalo
60
+ "Em Contato"), gargalo na primeira etapa, funil vazio (taxas `null`,
61
+ gargalo `null`) e funil sem gargalo (quedas zero).
62
+
40
63
  ## [0.1.21] — 2026-11-09
41
64
 
42
65
  ### Adicionado
package/backend/main.py CHANGED
@@ -602,6 +602,41 @@ def salvar_prompt_agente(agent_id: str, dados: dict):
602
602
  return {"ok": True, "agent_id": agent_id, "prompt": agente["prompt"]}
603
603
 
604
604
 
605
+ # ---------------------------------------------------------------------------
606
+ # Funil de Prospecção (métricas de conversão do CRM)
607
+ # ---------------------------------------------------------------------------
608
+
609
+ ETAPAS_FUNIL = ["Leads", "Em Contato", "Proposta", "Fechado"]
610
+
611
+
612
+ @app.get("/api/funnel")
613
+ def funil_prospeccao():
614
+ """Métricas de conversão do funil, calculadas a partir do leads.json."""
615
+ leads = storage.list_leads()
616
+ quantidades = [
617
+ sum(1 for l in leads if l.get("etapa") == etapa)
618
+ for etapa in ("leads", "em_contato", "proposta", "fechado")
619
+ ]
620
+ etapas = [{"nome": nome, "quantidade": qtd} for nome, qtd in zip(ETAPAS_FUNIL, quantidades)]
621
+
622
+ conversoes = []
623
+ for i in range(len(ETAPAS_FUNIL) - 1):
624
+ de, para = ETAPAS_FUNIL[i], ETAPAS_FUNIL[i + 1]
625
+ if quantidades[i] > 0:
626
+ taxa = round(quantidades[i + 1] / quantidades[i] * 100, 1)
627
+ queda = round(100 - taxa, 1)
628
+ else:
629
+ taxa, queda = None, None
630
+ conversoes.append({"de": de, "para": para, "taxa": taxa, "queda": queda})
631
+
632
+ # Gargalo: etapa ORIGEM da maior queda entre etapas consecutivas — é onde os
633
+ # leads param (ex.: muitos entram em "Em Contato" e não seguem).
634
+ quedas = [c for c in conversoes if c["queda"] is not None and c["queda"] > 0]
635
+ gargalo = max(quedas, key=lambda c: c["queda"])["de"] if quedas else None
636
+
637
+ return {"etapas": etapas, "conversoes": conversoes, "gargalo": gargalo}
638
+
639
+
605
640
  # ---------------------------------------------------------------------------
606
641
  # Configuração e integrações
607
642
  # ---------------------------------------------------------------------------
@@ -0,0 +1,65 @@
1
+ """Testes do Funil de Prospecção — GET /api/funnel.
2
+
3
+ Semeadura determinística no leads.json do home temporário para validar
4
+ contagens, taxas de conversão e identificação do gargalo.
5
+ """
6
+
7
+ import json
8
+
9
+ import pytest
10
+ from fastapi.testclient import TestClient
11
+
12
+ import main
13
+
14
+
15
+ @pytest.fixture()
16
+ def client(tmp_path, monkeypatch):
17
+ monkeypatch.setenv("LEADMASTER_HOME", str(tmp_path))
18
+ with TestClient(main.app) as c:
19
+ yield c
20
+
21
+
22
+ def _semear(client, distribuicao):
23
+ """Escreve leads diretamente no leads.json do home do teste."""
24
+ home = main.storage.storage_dir()
25
+ leads = []
26
+ for etapa, qtd in distribuicao.items():
27
+ for i in range(qtd):
28
+ leads.append({"id": f"{etapa}-{i}", "nome": f"Lead {etapa} {i}", "telefone": "", "email": "", "fonte": "Teste", "etapa": etapa})
29
+ (home / "leads.json").write_text(json.dumps(leads), encoding="utf-8")
30
+
31
+
32
+ def test_funil_conversoes_e_gargalo(client):
33
+ # [10, 5, 2, 1]: taxas 50%, 40%, 50% → quedas 50, 60, 50 → gargalo "Em Contato"
34
+ _semear(client, {"leads": 10, "em_contato": 5, "proposta": 2, "fechado": 1})
35
+ r = client.get("/api/funnel")
36
+ assert r.status_code == 200
37
+ corpo = r.json()
38
+ assert [e["quantidade"] for e in corpo["etapas"]] == [10, 5, 2, 1]
39
+ assert [e["nome"] for e in corpo["etapas"]] == ["Leads", "Em Contato", "Proposta", "Fechado"]
40
+ taxas = [c["taxa"] for c in corpo["conversoes"]]
41
+ assert taxas == [50.0, 40.0, 50.0]
42
+ assert corpo["gargalo"] == "Em Contato"
43
+
44
+
45
+ def test_funil_gargalo_primeira_etapa(client):
46
+ # Todos param na primeira etapa: queda 100% em Leads → Em Contato.
47
+ _semear(client, {"leads": 7, "em_contato": 0, "proposta": 0, "fechado": 0})
48
+ corpo = client.get("/api/funnel").json()
49
+ assert corpo["gargalo"] == "Leads"
50
+ assert corpo["conversoes"][0]["taxa"] == 0.0
51
+
52
+
53
+ def test_funil_sem_leads(client):
54
+ _semear(client, {})
55
+ corpo = client.get("/api/funnel").json()
56
+ assert [e["quantidade"] for e in corpo["etapas"]] == [0, 0, 0, 0]
57
+ assert all(c["taxa"] is None for c in corpo["conversoes"])
58
+ assert corpo["gargalo"] is None
59
+
60
+
61
+ def test_funil_sem_gargalo(client):
62
+ # Quedas todas zero (contagens iguais) → sem gargalo definido.
63
+ _semear(client, {"leads": 4, "em_contato": 4, "proposta": 4, "fechado": 4})
64
+ corpo = client.get("/api/funnel").json()
65
+ assert corpo["gargalo"] is None