/* Aba Captura: wizard UF → cidades → nichos → fontes → rodar, com progresso SSE. */ const Captura = (() => { let ufs = []; let municipios = []; // da UF selecionada let segments = []; let sources = []; const cidadesEscolhidas = new Set(); const nichosEscolhidos = new Set(); const cnaesAvulsos = new Map(); // codigo -> descricao let stopWatcher = null; // ---------- carregamento ---------- async function init() { const [ufsData, segData, srcData] = await Promise.all([ API.get('/api/meta/ufs'), API.get('/api/meta/segments'), API.get('/api/meta/sources') ]); ufs = ufsData.ufs; segments = segData.segments; sources = srcData.sources; renderUfs(); renderNichos(); renderFontes(); bindEvents(); await onUfChange(); await carregarHistorico(); await retomarJobAtivo(); } function renderUfs() { const sel = el('sel-uf'); sel.replaceChildren(...ufs.map((u) => h('option', { value: u.uf, selected: u.uf === 'BA' ? '' : null }, `${u.uf} — ${u.nome}${u.extraida ? ' ✓' : ''}`) )); } function renderNichos(filtro = '') { const grid = el('nichos-grid'); const termo = filtro.toLowerCase(); grid.replaceChildren(...segments .filter((s) => !termo || s.label.toLowerCase().includes(termo)) .map((s) => h('label', {}, h('input', { type: 'checkbox', value: s.name, checked: nichosEscolhidos.has(s.name) ? '' : null, onchange: (e) => { if (e.target.checked) nichosEscolhidos.add(s.name); else nichosEscolhidos.delete(s.name); atualizarContagem(); } }), s.label ))); el('nichos-contagem').textContent = `(${segments.length} disponíveis)`; } function atualizarContagem() { const modo = document.querySelector('input[name="nichos-modo"]:checked').value; el('nichos-contagem').textContent = modo === 'todos' ? `(${segments.length} disponíveis)` : `(${nichosEscolhidos.size} de ${segments.length} selecionados)`; } function renderFontes() { const box = el('fontes-lista'); box.replaceChildren(...sources.map((s) => { const input = h('input', { type: 'checkbox', value: s.id, checked: s.id === 'cnpj-base' ? '' : null, disabled: s.disponivel ? null : '' }); return h('label', { class: `fonte${s.disponivel ? '' : ' indisponivel'}` }, input, h('div', {}, h('div', {}, h('strong', {}, s.label), s.recomendada ? ' ⭐' : '', s.gratuita ? '' : ' 💰'), h('div', { class: 'desc' }, s.descricao + (s.disponivel ? '' : ` — ${s.motivo}`)) ) ); })); } // ---------- UF / extração ---------- function ufAtual() { return ufs.find((u) => u.uf === el('sel-uf').value); } async function onUfChange() { const u = ufAtual(); const status = el('uf-status'); if (u.extraindo) { status.textContent = '⏳ extraindo base...'; status.className = 'chip warn'; } else if (u.extraida) { status.textContent = '✓ base CNPJ pronta'; status.className = 'chip ok'; } else { status.textContent = 'base CNPJ não extraída'; status.className = 'chip warn'; } el('extracao-aviso').classList.toggle('hidden', u.extraida || u.extraindo); el('extracao-uf').textContent = `${u.uf} — ${u.nome}`; cidadesEscolhidas.clear(); renderChips(); const data = await API.get(`/api/meta/municipios?uf=${u.uf}`); municipios = data.municipios; } async function extrairBase() { const u = ufAtual(); try { const { jobId } = await API.post('/api/extractions', { uf: u.uf }); toast(`Extração de ${u.uf} iniciada — pode levar até 1 hora`); acompanharJob(jobId, `Extraindo base CNPJ de ${u.uf}`); u.extraindo = true; await onUfChange(); } catch (error) { toast(`Erro: ${error.message}`); } } // ---------- cidades ---------- function renderChips() { el('cidades-chips').replaceChildren(...[...cidadesEscolhidas].map((nome) => h('span', { class: 'chip', title: 'remover', onclick: () => { cidadesEscolhidas.delete(nome); renderChips(); } }, nome) )); } function buscarCidade(termo) { const box = el('cidades-sugestoes'); if (!termo || termo.length < 2) { box.replaceChildren(); return; } const t = termo.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, ''); const achados = municipios .filter((m) => m.nome.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').includes(t)) .slice(0, 12); box.replaceChildren(...achados.map((m) => h('button', { type: 'button', onclick: () => { cidadesEscolhidas.add(m.nome); renderChips(); el('busca-cidade').value = ''; box.replaceChildren(); } }, m.nome) )); } // ---------- CNAE avulso (qualquer atividade da Receita, alem dos 57 nichos) ---------- function renderCnaeChips() { el('cnae-chips').replaceChildren(...[...cnaesAvulsos].map(([codigo, descricao]) => h('span', { class: 'chip', title: 'remover', onclick: () => { cnaesAvulsos.delete(codigo); renderCnaeChips(); } }, descricao) )); } let buscaCnaeTimer = null; function buscarCnae(termo) { clearTimeout(buscaCnaeTimer); const box = el('cnae-sugestoes'); if (!termo || termo.trim().length < 2) { box.replaceChildren(); return; } buscaCnaeTimer = setTimeout(async () => { const { cnaes } = await API.get(`/api/meta/cnaes?q=${encodeURIComponent(termo)}`); box.replaceChildren(...cnaes.map((c) => h('button', { type: 'button', onclick: () => { cnaesAvulsos.set(c.codigo, c.descricao); renderCnaeChips(); el('busca-cnae').value = ''; box.replaceChildren(); } }, h('span', {}, c.descricao), h('span', { class: 'cod' }, c.codigo)) )); if (!cnaes.length) { box.replaceChildren(h('div', { class: 'muted' }, 'Nenhuma atividade encontrada')); } }, 250); } // ---------- captura ---------- function montarPayload() { const fontes = [...document.querySelectorAll('#fontes-lista input:checked')].map((i) => i.value); const cidadesModo = document.querySelector('input[name="cidades-modo"]:checked').value; const nichosModo = document.querySelector('input[name="nichos-modo"]:checked').value; const cnaeIds = [...cnaesAvulsos.keys()].map((codigo) => `cnae:${codigo}`); return { sources: fontes, uf: el('sel-uf').value, cities: cidadesModo === 'todas' ? ['all'] : [...cidadesEscolhidas], segments: [...(nichosModo === 'todos' ? ['all'] : [...nichosEscolhidos]), ...cnaeIds], limits: { maxPerQuery: Number(el('inp-max').value) || 10, queryLimit: Number(el('inp-query-limit').value) || 0 } }; } async function iniciarCaptura() { const payload = montarPayload(); if (!payload.sources.length) { toast('Selecione pelo menos uma fonte de busca'); return; } if (payload.cities.length === 0) { toast('Escolha pelo menos uma cidade (ou marque "todas")'); return; } if (payload.segments.length === 0) { toast('Escolha pelo menos um nicho (ou marque "todos")'); return; } try { const { jobId } = await API.post('/api/capture/runs', payload); acompanharJob(jobId, 'Captura em andamento'); } catch (error) { if (error.data?.precisaExtracao) { el('extracao-aviso').classList.remove('hidden'); toast(error.message); } else { toast(`Erro: ${error.message}`); } } } // ---------- progresso ---------- function acompanharJob(jobId, titulo) { const card = el('progresso-card'); card.classList.remove('hidden'); card.dataset.jobId = jobId; el('progresso-titulo').textContent = titulo; el('progresso-msg').textContent = 'iniciando...'; el('progresso-log').textContent = ''; el('progresso-fill').style.width = '3%'; el('progresso-stats').replaceChildren(); el('btn-cancelar').disabled = false; el('btn-capturar').disabled = true; if (stopWatcher) stopWatcher(); stopWatcher = API.watchJob(jobId, { progress(p) { if (p.message) el('progresso-msg').textContent = p.message; if (typeof p.pct === 'number') el('progresso-fill').style.width = `${Math.max(3, p.pct)}%`; if (p.counts) { el('progresso-fill').style.width = '50%'; el('progresso-stats').replaceChildren( mini(p.counts.raw, 'encontrados'), mini(p.counts.new, 'novos'), mini(p.counts.dup, 'já capturados') ); } }, log(l) { const box = el('progresso-log'); box.textContent += `${l.message}\n`; box.scrollTop = box.scrollHeight; }, done(d) { el('progresso-fill').style.width = '100%'; const s = d.summary || {}; el('progresso-msg').textContent = s.cancelled ? 'Cancelado.' : s.totalRows !== undefined ? `✅ Base extraída: ${fmt.format(s.totalRows)} empresas (${fmt.format(s.withPhone)} com telefone)` : `✅ Concluído: ${fmt.format(s.new ?? 0)} leads novos (${fmt.format(s.dup ?? 0)} já capturados antes)`; finalizar(); }, error(e) { el('progresso-msg').textContent = `❌ Erro: ${e.message}`; finalizar(); } }); function mini(valor, rotulo) { return h('div', { class: 'mini' }, h('b', {}, fmt.format(valor ?? 0)), h('span', {}, rotulo)); } async function finalizar() { el('btn-cancelar').disabled = true; el('btn-capturar').disabled = false; await Promise.all([carregarHistorico(), recarregarUfs()]); window.Tratamento?.invalidar(); } } async function recarregarUfs() { ufs = (await API.get('/api/meta/ufs')).ufs; const atual = el('sel-uf').value; renderUfs(); el('sel-uf').value = atual; await onUfChange(); } async function cancelarJob() { const jobId = el('progresso-card').dataset.jobId; const ativos = (await API.get('/api/jobs/active')).jobs; const job = ativos.find((j) => j.jobId === jobId); if (!job) { toast('Nenhum job ativo para cancelar'); return; } const rota = job.type === 'extracao' ? `/api/extractions/${job.meta.extractionId}/cancel` : `/api/capture/runs/${job.meta.runId}/cancel`; try { await API.post(rota, {}); toast('Cancelamento solicitado — aguardando o processo parar...'); } catch (error) { toast(`Erro: ${error.message}`); } } async function retomarJobAtivo() { const ativos = (await API.get('/api/jobs/active')).jobs; if (!ativos.length) return; const job = ativos[0]; acompanharJob(job.jobId, job.type === 'extracao' ? `Extraindo base CNPJ de ${job.meta.uf}` : 'Captura em andamento'); } // ---------- histórico ---------- async function carregarHistorico() { const { runs } = await API.get('/api/capture/runs?limit=8'); el('btn-ver-historico').textContent = runs.length ? `🕓 Histórico de capturas (${runs.length})` : '🕓 Histórico de capturas'; const box = el('historico-capturas'); if (!runs.length) { box.replaceChildren(h('div', { class: 'muted' }, 'Nenhuma captura ainda. Configure ao lado e clique em Iniciar.')); return; } const statusLabel = { done: '✅ concluída', running: '⏳ rodando', pending: '⏳ na fila', cancelled: '⚪ cancelada', error: '❌ erro' }; box.replaceChildren(...runs.map((r) => h('div', { class: 'item' }, h('div', {}, h('div', {}, h('strong', {}, `#${r.id} · ${(r.params.sources || []).join(', ')} · ${r.params.uf || ''}`)), h('div', { class: 'sub' }, `${dataBr(r.created_at)} · ${statusLabel[r.status] || r.status}`, r.error ? ` · ${r.error.slice(0, 80)}` : '') ), h('div', { class: 'sub' }, r.status === 'done' ? `${fmt.format(r.new_count)} novos / ${fmt.format(r.dup_count)} repetidos` : '') ))); } // ---------- eventos ---------- function bindEvents() { el('sel-uf').addEventListener('change', onUfChange); el('btn-extrair').addEventListener('click', extrairBase); el('btn-capturar').addEventListener('click', iniciarCaptura); el('btn-cancelar').addEventListener('click', cancelarJob); el('btn-ver-historico').addEventListener('click', () => el('modal-historico').showModal()); el('btn-fechar-historico').addEventListener('click', () => el('modal-historico').close()); for (const radio of document.querySelectorAll('input[name="cidades-modo"]')) { radio.addEventListener('change', () => { el('cidades-picker').classList.toggle('hidden', radio.value !== 'escolher' || !radio.checked); }); } for (const radio of document.querySelectorAll('input[name="nichos-modo"]')) { radio.addEventListener('change', () => { el('nichos-picker').classList.toggle('hidden', radio.value !== 'escolher' || !radio.checked); atualizarContagem(); }); } el('busca-cidade').addEventListener('input', (e) => buscarCidade(e.target.value)); el('busca-nicho').addEventListener('input', (e) => renderNichos(e.target.value)); el('busca-cnae').addEventListener('input', (e) => buscarCnae(e.target.value)); el('btn-nichos-todos').addEventListener('click', () => { for (const input of document.querySelectorAll('#nichos-grid input')) { input.checked = true; nichosEscolhidos.add(input.value); } atualizarContagem(); }); el('btn-nichos-nenhum').addEventListener('click', () => { nichosEscolhidos.clear(); renderNichos(el('busca-nicho').value); atualizarContagem(); }); } return { init }; })();