feat: implementacao inicial do sistema LeadRadar

This commit is contained in:
2026-08-26 17:14:35 -03:00
commit eb64f9a0f6
43 changed files with 2888 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
import pandas as pd
import streamlit as st
from api_client import APIClient
def render_admin_users_view(api_client: APIClient):
st.title("🛡️ Gestão de Usuários & Segurança (Admin)")
user_info = st.session_state.get('user', {})
if user_info.get('role') != 'admin':
st.error("🚫 Acesso restrito! Esta página é exclusiva para administradores.")
return
c1, c2 = st.columns([2, 1])
with c1:
st.subheader("👥 Operadores e Administradores")
users = api_client.get_users()
if users:
df_users = pd.DataFrame([
{
'ID': u['id'],
'Nome': u['nome'],
'E-mail': u['email'],
'Perfil': u['role'].upper(),
'Ativo': 'Sim' if u['ativo'] else 'Não',
'Criado em': u['criado_em'][:10] if u.get('criado_em') else ''
}
for u in users
])
st.dataframe(df_users, use_container_width=True, hide_index=True)
st.markdown("### ❌ Excluir Operador")
user_to_delete = st.selectbox(
"Selecione um usuário para remover:",
options=[u for u in users if u['id'] != user_info.get('id')],
format_func=lambda u: f"{u['nome']} ({u['email']}) - {u['role'].upper()}"
)
if user_to_delete:
if st.button(f"🗑️ Confirmar Exclusão de {user_to_delete['nome']}", type="secondary"):
success, msg = api_client.delete_user(user_to_delete['id'])
if success:
st.success(msg)
st.rerun()
else:
st.error(msg)
else:
st.warning("Não foi possível carregar a lista de usuários.")
with c2:
with st.container(border=True):
st.subheader(" Novo Usuário")
nome = st.text_input("Nome Completo", key="add_nome")
email = st.text_input("E-mail", key="add_email")
password = st.text_input("Senha", type="password", key="add_pass")
role = st.selectbox("Perfil de Acesso", ["user", "admin"], format_func=lambda r: "Operador (User)" if r == "user" else "Administrador (Admin)")
if st.button("✨ Criar Usuário", type="primary", use_container_width=True):
if not nome or not email or not password:
st.error("Preencha todos os campos obrigatórios.")
else:
success, msg = api_client.create_user(nome, email, password, role)
if success:
st.success(msg)
st.rerun()
else:
st.error(msg)
+106
View File
@@ -0,0 +1,106 @@
import pandas as pd
import streamlit as st
from api_client import APIClient
def render_analytical_table_view(api_client: APIClient):
st.title("📊 Tabela Analítica & Exportação de Leads")
st.caption("Filtre, analise e exporte seus leads nos formatos CSV e JSON.")
leads = api_client.get_leads()
# Controles de Exportação
exp_col1, exp_col2, _ = st.columns([1, 1, 2])
with exp_col1:
csv_bytes = api_client.export_leads_data('csv')
st.download_button(
label="📥 Exportar em CSV",
data=csv_bytes,
file_name="leads_leadradar.csv",
mime="text/csv",
use_container_width=True
)
with exp_col2:
json_bytes = api_client.export_leads_data('json')
st.download_button(
label="📥 Exportar em JSON",
data=json_bytes,
file_name="leads_leadradar.json",
mime="application/json",
use_container_width=True
)
st.markdown("---")
# Filtros Avançados
f1, f2, f3, f4 = st.columns(4)
with f1:
search = st.text_input("🔍 Busca Geral:", value="", key="tbl_search")
with f2:
statuses = ["Todos", "novo", "contatado", "respondeu", "negociacao", "ganho", "perdido", "opt_out"]
status_sel = st.selectbox("Status Funil:", statuses, key="tbl_status")
with f3:
cidades = ["Todas"] + sorted(list(set(l.get('cidade', '') for l in leads if l.get('cidade'))))
cidade_sel = st.selectbox("Cidade:", cidades, key="tbl_cidade")
with f4:
ramos = ["Todos"] + sorted(list(set(l.get('ramo_atividade', '') for l in leads if l.get('ramo_atividade'))))
ramo_sel = st.selectbox("Ramo:", ramos, key="tbl_ramo")
# Aplicar filtros
filtered = leads
if search:
s = search.lower()
filtered = [l for l in filtered if s in l.get('nome_empresa', '').lower() or s in l.get('telefone', '').lower() or s in l.get('notas', '').lower()]
if status_sel != "Todos":
filtered = [l for l in filtered if l.get('status_funil') == status_sel]
if cidade_sel != "Todas":
filtered = [l for l in filtered if l.get('cidade') == cidade_sel]
if ramo_sel != "Todos":
filtered = [l for l in filtered if l.get('ramo_atividade') == ramo_sel]
st.write(f"**Exibindo {len(filtered)} leads de {len(leads)} totais.**")
if filtered:
rows = []
for l in filtered:
tel_san = l.get('telefone_sanitizado', '')
wa_link = f"https://wa.me/{tel_san}" if tel_san else ""
rows.append({
'ID': l.get('id'),
'Empresa': l.get('nome_empresa'),
'Ramo': l.get('ramo_atividade'),
'Cidade/UF': f"{l.get('cidade', '')}/{l.get('uf', '')}",
'Telefone': l.get('telefone'),
'Rating': f"{l.get('google_rating')} ({l.get('total_avaliacoes')})",
'Status': l.get('status_funil'),
'WhatsApp': wa_link,
'Criado em': l.get('criado_em', '')[:10] if l.get('criado_em') else ''
})
df = pd.DataFrame(rows)
st.dataframe(
df,
column_config={
"WhatsApp": st.column_config.LinkColumn("WhatsApp Link", display_text="💬 Conversar")
},
use_container_width=True,
hide_index=True
)
st.markdown("### ⚙️ Ações Rápidas")
col_sel, col_act = st.columns([2, 1])
with col_sel:
selected_lead_id = st.selectbox(
"Selecione um Lead para Ação LGPD:",
options=[l['id'] for l in filtered],
format_func=lambda x: next((f"{l['nome_empresa']} ({l['status_funil']})" for l in filtered if l['id'] == x), x)
)
with col_act:
if st.button("🚫 Marcar Opt-Out (LGPD)", type="secondary", use_container_width=True):
if selected_lead_id:
api_client.opt_out_lead(selected_lead_id)
st.success("Lead marcado como Opt-Out (Excluído / LGPD).")
st.rerun()
else:
st.info("Nenhum lead encontrado com os filtros selecionados.")
+101
View File
@@ -0,0 +1,101 @@
import streamlit as st
from api_client import APIClient
from components.kanban import render_kanban_board
from components.metrics import render_metrics_summary
def render_crm_kanban_view(api_client: APIClient):
st.title("📌 Funil Commercial CRM (Kanban)")
# Carregar leads da API
leads = api_client.get_leads()
# Barra de Filtros
fc1, fc2, fc3 = st.columns([2, 1, 1])
with fc1:
search_query = st.text_input("🔍 Buscar por nome, telefone ou nota:", value="", key="kanban_search")
with fc2:
ramos = sorted(list(set(l.get('ramo_atividade', '') for l in leads if l.get('ramo_atividade'))))
selected_ramo = st.selectbox("Filtrar Ramo:", ["Todos"] + ramos, key="kanban_ramo")
with fc3:
cidades = sorted(list(set(l.get('cidade', '') for l in leads if l.get('cidade'))))
selected_cidade = st.selectbox("Filtrar Cidade:", ["Todas"] + cidades, key="kanban_cidade")
# Aplicar filtros locais
filtered_leads = leads
if search_query:
sq = search_query.lower()
filtered_leads = [
l for l in filtered_leads if
sq in l.get('nome_empresa', '').lower() or
sq in l.get('telefone', '').lower() or
sq in l.get('notas', '').lower()
]
if selected_ramo != "Todos":
filtered_leads = [l for l in filtered_leads if l.get('ramo_atividade') == selected_ramo]
if selected_cidade != "Todas":
filtered_leads = [l for l in filtered_leads if l.get('cidade') == selected_cidade]
# Exibir resumo de métricas KPI
render_metrics_summary(filtered_leads)
st.markdown("---")
# Exibir Modal de Detalhes se selecionado
selected_lead_id = st.session_state.get('selected_lead_id')
if selected_lead_id:
render_lead_detail_modal(selected_lead_id, api_client)
# Renderizar o Quadro Kanban
render_kanban_board(filtered_leads, api_client)
def render_lead_detail_modal(lead_id: str, api_client: APIClient):
lead_detail = api_client.get_lead_details(lead_id)
if not lead_detail:
st.session_state.pop('selected_lead_id', None)
return
with st.expander(f"📝 Detalhes e Histórico de Auditoria: {lead_detail.get('nome_empresa')}", expanded=True):
col1, col2 = st.columns([1, 1])
with col1:
st.subheader("Informações do Lead")
st.write(f"**Ramo:** {lead_detail.get('ramo_atividade')}")
st.write(f"**Telefone:** {lead_detail.get('telefone', 'N/A')}")
st.write(f"**Endereço:** {lead_detail.get('logradouro', '')}, {lead_detail.get('bairro', '')} - {lead_detail.get('cidade', '')}/{lead_detail.get('uf', '')}")
st.write(f"**Status Atual:** `{lead_detail.get('status_funil')}`")
new_notes = st.text_area("Notas / Observações Comerciais:", value=lead_detail.get('notas', ''), height=120)
c_save, c_opt, c_close = st.columns([1, 1, 1])
with c_save:
if st.button("💾 Salvar Notas"):
api_client.update_lead(lead_id, {'notas': new_notes})
st.success("Notas atualizadas!")
st.rerun()
with c_opt:
if st.button("🚫 Opt-Out (LGPD)", help="Marcar lead para não contatar"):
api_client.opt_out_lead(lead_id)
st.warning("Lead marcado como Opt-Out LGPD.")
st.session_state.pop('selected_lead_id', None)
st.rerun()
with c_close:
if st.button("❌ Fechar"):
st.session_state.pop('selected_lead_id', None)
st.rerun()
with col2:
st.subheader("📜 Histórico de Interações (Auditoria)")
interacoes = lead_detail.get('interacoes', [])
if not interacoes:
st.info("Nenhuma interação registrada ainda.")
else:
for idx in interacoes:
st.markdown(
f"""
<div style="background: #21262D; border: 1px solid #30363D; border-radius: 6px; padding: 8px; margin-bottom: 6px; font-size: 0.85rem;">
<strong>[{idx.get('tipo')}]</strong> {idx.get('descricao')}<br>
<small style="color: #8B949E;">Por: {idx.get('usuario_nome')} em {idx.get('timestamp')[:19] if idx.get('timestamp') else ''}</small>
</div>
""", unsafe_allow_html=True
)
+40
View File
@@ -0,0 +1,40 @@
import streamlit as st
from api_client import APIClient
def render_login_view(api_client: APIClient):
st.markdown("<br><br>", unsafe_allow_html=True)
c1, c2, c3 = st.columns([1, 2, 1])
with c2:
with st.container(border=True):
st.markdown(
"""
<div style="text-align: center; margin-bottom: 24px;">
<h1 style="color: #58A6FF; margin-bottom: 4px;">📡 LeadRadar</h1>
<p style="color: #8B949E; font-size: 0.95rem;">Prospecção Inteligente & Gestão Comercial B2B</p>
</div>
""", unsafe_allow_html=True
)
email = st.text_input("E-mail de Acesso", placeholder="seu.email@empresa.com", key="login_email")
password = st.text_input("Senha", type="password", placeholder="••••••••", key="login_password")
if st.button("🚀 Entrar no Sistema", use_container_width=True, type="primary"):
if not email or not password:
st.error("Por favor, preencha o e-mail e a senha.")
else:
with st.spinner("Autenticando credenciais..."):
success, message = api_client.login(email, password)
if success:
st.success(message)
st.rerun()
else:
st.error(message)
st.markdown(
"""
<div style="text-align: center; margin-top: 16px; font-size: 0.8rem; color: #8B949E;">
LeadRadar Core v1.0 • Desenvolvido com Flask & Streamlit
</div>
""", unsafe_allow_html=True
)
+38
View File
@@ -0,0 +1,38 @@
import streamlit as st
from api_client import APIClient
def render_profile_view(api_client: APIClient):
st.title("👤 Meu Perfil & Segurança")
user_info = st.session_state.get('user', {})
c1, c2 = st.columns([1, 1])
with c1:
with st.container(border=True):
st.subheader("📋 Dados da Conta")
st.write(f"**Nome:** {user_info.get('nome', 'N/A')}")
st.write(f"**E-mail:** {user_info.get('email', 'N/A')}")
st.write(f"**Perfil:** `{user_info.get('role', 'user').upper()}`")
st.write(f"**Status da Conta:** {'🟢 Ativo' if user_info.get('ativo') else '🔴 Inativo'}")
with c2:
with st.container(border=True):
st.subheader("🔒 Alterar Minha Senha")
old_pass = st.text_input("Senha Atual", type="password", key="pwd_old")
new_pass = st.text_input("Nova Senha (min 6 caracteres)", type="password", key="pwd_new")
confirm_pass = st.text_input("Confirmar Nova Senha", type="password", key="pwd_conf")
if st.button("🔑 Atualizar Senha", type="primary", use_container_width=True):
if not old_pass or not new_pass or not confirm_pass:
st.error("Preencha todos os campos de senha.")
elif new_pass != confirm_pass:
st.error("A nova senha e a confirmação não coincidem.")
elif len(new_pass) < 6:
st.error("A nova senha deve ter no mínimo 6 caracteres.")
else:
success, msg = api_client.change_password(old_pass, new_pass)
if success:
st.success("Senha alterada com sucesso! Faça login com sua nova senha.")
else:
st.error(msg)
+76
View File
@@ -0,0 +1,76 @@
import pandas as pd
import streamlit as st
from api_client import APIClient
def render_radar_busca_view(api_client: APIClient):
st.title("📡 Radar de Busca & Prospecção Geolocalizada")
st.caption("Consulte estabelecimentos comerciais no Google Maps a partir da triangulação de CEP e ramo de atividade.")
c1, c2 = st.columns([1, 2])
with c1:
with st.container(border=True):
st.subheader("🔍 Parâmetros de Varredura")
cep = st.text_input("CEP Alvo", value="01310-100", help="Digite o CEP com ou sem hífen (ex: 01310-100)")
ramo_sugestoes = [
"Clínica Odontológica", "Padaria", "Restaurante", "Academia",
"Escritório de Contabilidade", "Oficina Mecânica", "Farmácia", "Salão de Beleza"
]
ramo = st.selectbox("Ramo de Atividade", options=ramo_sugestoes, index=0)
custom_ramo = st.text_input("Ou digite um ramo personalizado:", value="", placeholder="Ex: Petshop 24h")
ramo_final = custom_ramo.strip() if custom_ramo.strip() else ramo
max_results = st.slider("Quantidade máxima de leads", min_value=5, max_value=50, value=15, step=5)
if st.button("🛰️ Disparar Prospecção Ativa", type="primary", use_container_width=True):
if not cep:
st.error("Informe um CEP válido.")
else:
progress_bar = st.progress(0, text="Iniciando triangulação de CEP via ViaCEP...")
with st.spinner("Extraindo estabelecimentos no Google Maps via Playwright Scraper..."):
progress_bar.progress(30, text="Bairro e Município resolvidos. Abrindo Playwright Chromium...")
success, data = api_client.search_maps(cep, ramo_final, max_results)
progress_bar.progress(80, text="Deduplicando e persistindo leads no banco de dados...")
if success:
progress_bar.progress(100, text="Varredura concluída!")
st.session_state['last_search_data'] = data
st.success("Busca executada e leads atualizados no banco de dados!")
else:
progress_bar.empty()
st.error(data.get('error', 'Falha ao executar prospecção.'))
with c2:
search_data = st.session_state.get('last_search_data')
if search_data:
summary = search_data.get('summary', {})
loc = summary.get('location', {})
leads = summary.get('leads', [])
st.subheader("📊 Resultado da Varredura")
mc1, mc2, mc3 = st.columns(3)
mc1.metric("Localização", f"{loc.get('bairro', 'Bairro')}, {loc.get('cidade', 'Cidade')}-{loc.get('uf', '')}")
mc2.metric("Novos Leads", summary.get('created_count', 0))
mc3.metric("Reencontrados/Atualizados", summary.get('updated_count', 0))
if leads:
st.markdown("### 📋 Preview dos Leads Obtidos")
df_data = []
for l in leads:
df_data.append({
'Empresa': l.get('nome_empresa'),
'Ramo': l.get('ramo_atividade'),
'Telefone': l.get('telefone'),
'Avaliação': f"{l.get('google_rating')} ({l.get('total_avaliacoes')})",
'Bairro/Cidade': f"{l.get('bairro', '')} / {l.get('cidade', '')}",
'Status': l.get('status_funil')
})
df = pd.DataFrame(df_data)
st.dataframe(df, use_container_width=True)
st.info("💡 Acesse o menu **Funil CRM (Kanban)** para gerenciar estes leads!")
else:
st.info("👈 Preencha os parâmetros no painel ao lado e clique em **Disparar Prospecção Ativa** para iniciar.")