@danhachuel/leadmaster 0.1.7 → 0.1.8

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 CHANGED
@@ -6,6 +6,28 @@ O formato é baseado no [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.
6
6
  A etiqueta visual de versão usa patch de dois dígitos (ex.: `0.1.00`); o registry NPM
7
7
  normaliza para a forma canônica (ex.: `0.1.0`).
8
8
 
9
+ ## [0.1.08] — 2026-11-08
10
+
11
+ ### Adicionado
12
+
13
+ - **Módulo CRM Kanban implementado conforme especificação:**
14
+ - Rotas: `GET /api/crm/leads`, `POST /api/crm/leads` (criação manual) e
15
+ `PUT /api/crm/leads/{lead_id}/etapa` (movimentação ao soltar o card) —
16
+ persistência no `leads.json` local (seeds de demonstração mantidos).
17
+ - Kanban com **@dnd-kit/core + @dnd-kit/sortable** (multi-contêiner):
18
+ arrastar entre colunas persiste via API; reordenação visual dentro da
19
+ coluna; soltar fora das colunas reverte o movimento.
20
+ - Colunas: **Novos Leads → Em Contato → Proposta → Fechado**.
21
+ - Card mostra Nome, Telefone, Fonte e **Data de Criação**.
22
+ - Botão **"Adicionar Lead"** (gradiente) com modal de criação (campos
23
+ avançados em accordion).
24
+ - UX: card inteiro arrastável (Fitts); **opacity 0.5** no card arrastado e
25
+ **borda tracejada indigo** na coluna de destino (Zeigarnik); colunas
26
+ brancas com sombra suave (padrão do Dashboard).
27
+ - **7 novos testes pytest** para as rotas do CRM (`backend/tests/test_crm_routes.py`):
28
+ listagem, criação, persistência, validação, movimentação, 404 e etapas
29
+ inválidas — isolados com `LEADMASTER_HOME` temporário.
30
+
9
31
  ## [0.1.07] — 2026-11-08
10
32
 
11
33
  ### Adicionado
package/backend/main.py CHANGED
@@ -295,6 +295,42 @@ def aplicar_sugestao(dados: dict):
295
295
  return {"ok": True, "lead": atualizado}
296
296
 
297
297
 
298
+ # ---------------------------------------------------------------------------
299
+ # CRM — Kanban (leads por etapa)
300
+ # ---------------------------------------------------------------------------
301
+
302
+ @app.get("/api/crm/leads")
303
+ def crm_listar_leads():
304
+ """Todos os leads (persistidos em leads.json) — usado pelo quadro Kanban."""
305
+ return storage.list_leads()
306
+
307
+
308
+ @app.post("/api/crm/leads", status_code=201)
309
+ def crm_criar_lead(lead_in: LeadIn):
310
+ """Cria um lead manualmente pelo modal 'Adicionar Lead' do CRM."""
311
+ lead = lead_in.model_dump()
312
+ lead["id"] = storage._novo_id()
313
+ lead["etapa"] = _validar_etapa(lead.get("etapa", "leads"))
314
+ lead["score"] = lead.get("score") if lead.get("score") is not None else crm_agent.calcular_completude(lead)
315
+ storage.add_lead(lead)
316
+ agents.disparar_webhook("lead.criado", {"lead_id": lead["id"], "nome": lead["nome"]})
317
+ return lead
318
+
319
+
320
+ @app.put("/api/crm/leads/{lead_id}/etapa")
321
+ def crm_mover_lead(lead_id: str, dados: dict):
322
+ """Move um lead de coluna no Kanban (drag-and-drop)."""
323
+ etapa = dados.get("etapa")
324
+ if not etapa:
325
+ raise HTTPException(400, "Informe a etapa de destino.")
326
+ _validar_etapa(etapa)
327
+ atualizado = storage.update_lead(lead_id, {"etapa": etapa})
328
+ if not atualizado:
329
+ raise HTTPException(404, "Lead não encontrado.")
330
+ agents.disparar_webhook("lead.etapa", {"lead_id": lead_id, "etapa": etapa, "via": "kanban"})
331
+ return atualizado
332
+
333
+
298
334
  # ---------------------------------------------------------------------------
299
335
  # Campanhas / Disparos
300
336
  # ---------------------------------------------------------------------------
@@ -0,0 +1,64 @@
1
+ """Testes das rotas do CRM Kanban (backend/main.py).
2
+
3
+ Usam TestClient do FastAPI com LEADMASTER_HOME em diretório temporário, para
4
+ nunca tocar nos dados reais do usuário. Rodam iguais no CI (Linux) e local.
5
+ """
6
+
7
+ import pytest
8
+ from fastapi.testclient import TestClient
9
+
10
+ import main
11
+
12
+
13
+ @pytest.fixture()
14
+ def client(tmp_path, monkeypatch):
15
+ monkeypatch.setenv("LEADMASTER_HOME", str(tmp_path))
16
+ with TestClient(main.app) as c:
17
+ yield c
18
+
19
+
20
+ def test_listar_leads(client):
21
+ r = client.get("/api/crm/leads")
22
+ assert r.status_code == 200
23
+ corpo = r.json()
24
+ assert isinstance(corpo, list) and len(corpo) >= 1 # seeds de demonstração
25
+
26
+
27
+ def test_criar_lead(client):
28
+ r = client.post("/api/crm/leads", json={"nome": "Padaria Teste", "telefone": "(11) 90000-0000", "fonte": "Manual"})
29
+ assert r.status_code == 201
30
+ lead = r.json()
31
+ assert lead["id"]
32
+ assert lead["etapa"] == "leads"
33
+ # Confirma persistência no JSON local
34
+ lista = client.get("/api/crm/leads").json()
35
+ assert any(l["id"] == lead["id"] for l in lista)
36
+
37
+
38
+ def test_criar_lead_sem_nome_invalido(client):
39
+ r = client.post("/api/crm/leads", json={"nome": ""})
40
+ assert r.status_code == 422 # validação do modelo Pydantic
41
+
42
+
43
+ def test_mover_lead_entre_colunas(client):
44
+ criado = client.post("/api/crm/leads", json={"nome": "Lead Móvel"}).json()
45
+ r = client.put(f"/api/crm/leads/{criado['id']}/etapa", json={"etapa": "proposta"})
46
+ assert r.status_code == 200
47
+ assert r.json()["etapa"] == "proposta"
48
+
49
+
50
+ def test_mover_lead_inexistente(client):
51
+ r = client.put("/api/crm/leads/nao-existe/etapa", json={"etapa": "fechado"})
52
+ assert r.status_code == 404
53
+
54
+
55
+ def test_mover_lead_etapa_invalida(client):
56
+ criado = client.post("/api/crm/leads", json={"nome": "Lead X"}).json()
57
+ r = client.put(f"/api/crm/leads/{criado['id']}/etapa", json={"etapa": "nao_existe"})
58
+ assert r.status_code == 400
59
+
60
+
61
+ def test_mover_lead_sem_etapa(client):
62
+ criado = client.post("/api/crm/leads", json={"nome": "Lead Y"}).json()
63
+ r = client.put(f"/api/crm/leads/{criado['id']}/etapa", json={})
64
+ assert r.status_code == 400