feat: implementacao inicial do sistema LeadRadar
This commit is contained in:
@@ -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)
|
||||
@@ -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.")
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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.")
|
||||
Reference in New Issue
Block a user