@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.
- package/CHANGELOG.md +23 -0
- package/backend/__pycache__/main.cpython-311.pyc +0 -0
- package/backend/main.py +35 -0
- package/backend/services/__pycache__/__init__.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/agents.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/crm_agent.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/extractors.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/flags.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/llm.cpython-311.pyc +0 -0
- package/backend/services/__pycache__/storage.cpython-311.pyc +0 -0
- package/backend/tests/__pycache__/conftest.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_agents_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_campaigns_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_cnpj_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_config.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_crm_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_funnel_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_instagram_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_launcher_protections.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_maps_extractor.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_metrics.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_process_flags.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/__pycache__/test_webhook_routes.cpython-311-pytest-8.4.2.pyc +0 -0
- package/backend/tests/test_funnel_routes.py +65 -0
- package/frontend/dist/assets/{index-CC4fz5Tw.js → index-CX_h90mG.js} +15 -15
- package/frontend/dist/assets/{index-Bn20U7qZ.css → index-ViDiQGJH.css} +1 -1
- package/frontend/dist/index.html +2 -2
- 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
|
|
Binary file
|
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
|
# ---------------------------------------------------------------------------
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|