feat: selecao individual de leads antes de salvar, dark mode global e extracao 100% real do Google Maps
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
[theme]
|
||||
primaryColor = "#58A6FF"
|
||||
backgroundColor = "#0D1117"
|
||||
secondaryBackgroundColor = "#161B22"
|
||||
textColor = "#E6EDF3"
|
||||
font = "sans serif"
|
||||
|
||||
[server]
|
||||
headless = true
|
||||
enableCORS = false
|
||||
enableXsrfProtection = false
|
||||
+26
-4
@@ -50,8 +50,11 @@ class APIClient:
|
||||
except Exception as e:
|
||||
return False, f"Erro ao conectar com API: {str(e)}"
|
||||
|
||||
def search_maps(self, cep: str, ramo: str, max_results: int = 15) -> tuple[bool, dict]:
|
||||
url = f"{self.base_url}/api/v1/leads/search-maps"
|
||||
def scrape_preview(self, cep: str, ramo: str, max_results: int = 15) -> tuple[bool, dict]:
|
||||
"""
|
||||
Solicita a varredura no Maps sem salvar no banco de dados ainda.
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/leads/scrape-preview"
|
||||
try:
|
||||
with httpx.Client(timeout=60.0) as client:
|
||||
res = client.post(url, headers=self._get_headers(), json={
|
||||
@@ -62,9 +65,28 @@ class APIClient:
|
||||
data = res.json()
|
||||
if res.status_code == 200:
|
||||
return True, data
|
||||
return False, {'error': data.get('error', 'Erro durante varredura no Google Maps.')}
|
||||
return False, {'error': data.get('error', 'Erro durante prospecção no Google Maps.')}
|
||||
except Exception as e:
|
||||
return False, {'error': f"Erro ao processar busca: {str(e)}"}
|
||||
return False, {'error': f"Erro ao conectar com API: {str(e)}"}
|
||||
|
||||
def import_selected_leads(self, cep: str, ramo: str, selected_leads: list) -> tuple[bool, str]:
|
||||
"""
|
||||
Envia a lista de leads selecionados para serem salvos no banco.
|
||||
"""
|
||||
url = f"{self.base_url}/api/v1/leads/import-selected"
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
res = client.post(url, headers=self._get_headers(), json={
|
||||
'cep': cep,
|
||||
'ramo': ramo,
|
||||
'selected_leads': selected_leads
|
||||
})
|
||||
data = res.json()
|
||||
if res.status_code == 200:
|
||||
return True, data.get('message', 'Leads importados com sucesso!')
|
||||
return False, data.get('error', 'Falha ao importar leads.')
|
||||
except Exception as e:
|
||||
return False, f"Erro ao conectar com API: {str(e)}"
|
||||
|
||||
def get_leads(self, status_funil: str = None, cidade: str = None, ramo: str = None, search: str = None) -> list:
|
||||
url = f"{self.base_url}/api/v1/leads"
|
||||
|
||||
@@ -20,10 +20,22 @@
|
||||
--status-optout: #8B949E;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
/* Global Dark Mode Overrides */
|
||||
html, body, [data-testid="stAppViewContainer"], [data-testid="stHeader"] {
|
||||
background-color: var(--bg-primary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
[data-testid="stSidebar"] {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border-right: 1px solid var(--border-color) !important;
|
||||
}
|
||||
|
||||
/* Dataframe and Tables Dark Styling */
|
||||
[data-testid="stDataFrame"] {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
/* Custom Metric Card */
|
||||
@@ -34,7 +46,7 @@ body {
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
|
||||
@@ -3,14 +3,14 @@ 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.")
|
||||
st.title("📡 Radar de Prospecção Geolocalizada")
|
||||
st.caption("Varra o Google Maps em tempo real por CEP e ramo de atividade. Selecione individualmente quais estabelecimentos deseja importar para a sua base de dados.")
|
||||
|
||||
c1, c2 = st.columns([1, 2])
|
||||
|
||||
with c1:
|
||||
with st.container(border=True):
|
||||
st.subheader("🔍 Parâmetros de Varredura")
|
||||
st.subheader("🔍 Parâmetros de Busca")
|
||||
|
||||
cep = st.text_input("CEP Alvo", value="01310-100", help="Digite o CEP com ou sem hífen (ex: 01310-100)")
|
||||
|
||||
@@ -22,55 +22,98 @@ def render_radar_busca_view(api_client: APIClient):
|
||||
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)
|
||||
max_results = st.slider("Quantidade máxima de estabelecimentos", min_value=5, max_value=50, value=15, step=5)
|
||||
|
||||
if st.button("🛰️ Disparar Prospecção Ativa", type="primary", use_container_width=True):
|
||||
if st.button("🛰️ Buscar no Google Maps", type="primary", use_container_width=True):
|
||||
if not cep:
|
||||
st.error("Informe um CEP válido.")
|
||||
else:
|
||||
st.session_state.pop('preview_data', None)
|
||||
st.session_state.pop('import_success_msg', None)
|
||||
|
||||
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...")
|
||||
with st.spinner("Navegando no Google Maps em tempo real via Playwright Chromium..."):
|
||||
progress_bar.progress(40, text="Executando scraper headless e rolando feed de estabelecimentos...")
|
||||
success, data = api_client.scrape_preview(cep, ramo_final, max_results)
|
||||
progress_bar.progress(100, text="Varredura concluída!")
|
||||
|
||||
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!")
|
||||
st.session_state['preview_data'] = data
|
||||
st.session_state['search_cep'] = cep
|
||||
st.session_state['search_ramo'] = ramo_final
|
||||
else:
|
||||
progress_bar.empty()
|
||||
st.error(data.get('error', 'Falha ao executar prospecção.'))
|
||||
st.error(data.get('error', 'Falha ao executar varredura.'))
|
||||
|
||||
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', [])
|
||||
# Se houver mensagem de sucesso da importação recente
|
||||
if st.session_state.get('import_success_msg'):
|
||||
st.success(st.session_state['import_success_msg'])
|
||||
|
||||
st.subheader("📊 Resultado da Varredura")
|
||||
preview_data = st.session_state.get('preview_data')
|
||||
if preview_data:
|
||||
results = preview_data.get('results', [])
|
||||
location = preview_data.get('location', {})
|
||||
count = preview_data.get('count', 0)
|
||||
|
||||
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))
|
||||
st.subheader(f"📋 Estabelecimentos Encontrados ({count})")
|
||||
st.caption(f"📍 **Localização:** {location.get('bairro', 'Bairro')}, {location.get('cidade', 'Cidade')}-{location.get('uf', '')} | Query: `{preview_data.get('search_query')}`")
|
||||
|
||||
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!")
|
||||
if not results:
|
||||
st.warning("Nenhum estabelecimento foi encontrado no Google Maps para esta combinação de CEP e Ramo.")
|
||||
else:
|
||||
st.write("Marque as caixas de seleção abaixo para escolher quais leads deseja salvar no banco de dados:")
|
||||
|
||||
# Tabela com checkboxes de seleção individual
|
||||
df_preview = pd.DataFrame([
|
||||
{
|
||||
'Importar': True,
|
||||
'Empresa': r.get('nome_empresa'),
|
||||
'Telefone': r.get('telefone') or 'Não informado',
|
||||
'Avaliação': f"⭐ {r.get('google_rating')} ({r.get('total_avaliacoes')})",
|
||||
'Endereço / Trecho': r.get('endereco', ''),
|
||||
'_raw_item': r
|
||||
}
|
||||
for r in results
|
||||
])
|
||||
|
||||
edited_df = st.data_editor(
|
||||
df_preview.drop(columns=['_raw_item']),
|
||||
column_config={
|
||||
"Importar": st.column_config.CheckboxColumn("Importar?", default=True)
|
||||
},
|
||||
disabled=["Empresa", "Telefone", "Avaliação", "Endereço / Trecho"],
|
||||
use_container_width=True,
|
||||
hide_index=True
|
||||
)
|
||||
|
||||
# Identificar os itens marcados para importação
|
||||
selected_indices = edited_df[edited_df["Importar"]].index.tolist()
|
||||
selected_items = [results[i] for i in selected_indices if i < len(results)]
|
||||
|
||||
st.write(f"**Leads selecionados para importação:** {len(selected_items)} de {len(results)}")
|
||||
|
||||
btn_c1, btn_c2 = st.columns([2, 1])
|
||||
with btn_c1:
|
||||
if st.button(f"💾 Salvar {len(selected_items)} Lead(s) Selecionado(s) no Banco", type="primary", use_container_width=True, disabled=len(selected_items) == 0):
|
||||
with st.spinner("Persistindo e deduplicando leads selecionados no banco..."):
|
||||
imp_success, imp_msg = api_client.import_selected_leads(
|
||||
st.session_state.get('search_cep', cep),
|
||||
st.session_state.get('search_ramo', ramo_final),
|
||||
selected_items
|
||||
)
|
||||
if imp_success:
|
||||
st.session_state['import_success_msg'] = f"✅ {imp_msg}"
|
||||
st.session_state.pop('preview_data', None)
|
||||
st.rerun()
|
||||
else:
|
||||
st.error(imp_msg)
|
||||
|
||||
with btn_c2:
|
||||
if st.button("❌ Cancelar", use_container_width=True):
|
||||
st.session_state.pop('preview_data', None)
|
||||
st.rerun()
|
||||
else:
|
||||
st.info("👈 Preencha os parâmetros no painel ao lado e clique em **Disparar Prospecção Ativa** para iniciar.")
|
||||
if not st.session_state.get('import_success_msg'):
|
||||
st.info("👈 Insira os parâmetros ao lado e clique em **Buscar no Google Maps** para visualizar e escolher os leads.")
|
||||
|
||||
Reference in New Issue
Block a user