Files
COMERCIAL-GTI/public/js/tratamento.js
T

690 lines
29 KiB
JavaScript

/* Aba Tratamento: dashboard, navegador de leads com filtros e exportações. */
const Tratamento = (() => {
let page = 1;
let totalPages = 1;
let carregado = false;
let refreshTimer = null;
const CLASSIFICACAO_LABELS = {
excelente: 'Excelente', bom: 'Bom', medio: 'Médio', baixo: 'Baixo', muito_baixo: 'Muito Baixo'
};
const PRIORIDADE_LABELS = {
1: 'P1 — atacar imediatamente', 2: 'P2 — trabalhar durante a semana',
3: 'P3 — quando não houver prioritários', 4: 'P4 — só em campanhas específicas'
};
const WHATSAPP_STATUS_LABELS = {
nao_consultado: 'Não consultado',
confirmado: 'Confirmado',
nao_cadastrado: 'Não cadastrado',
erro_temporario: 'Erro temporário'
};
function estrelas(n) {
const cheias = Math.max(0, Math.min(5, Number(n) || 0));
return '★'.repeat(cheias) + '☆'.repeat(5 - cheias);
}
// ---------- filtros ----------
function filtroAtual() {
const filtro = {};
for (const input of document.querySelectorAll('#filtros [data-filtro]')) {
const value = input.value.trim();
if (value) filtro[input.dataset.filtro] = value;
}
return filtro;
}
function filtroQuery(extra = {}) {
return new URLSearchParams({ ...filtroAtual(), ...extra }).toString();
}
// Clique nos cards/barras do dashboard: aplica o filtro somando aos ja
// ativos; clicar de novo no mesmo valor remove so aquele filtro.
function toggleFiltro(inputId, valor) {
const input = el(inputId);
const novo = String(valor ?? '');
input.value = input.value === novo ? '' : novo;
atualizar();
}
function descreverFiltro() {
const f = filtroAtual();
const partes = [];
if (f.uf) partes.push(`UF ${f.uf}`);
if (f.cidade) partes.push(f.cidade);
if (f.segmento) partes.push(el('f-segmento').selectedOptions[0]?.textContent || f.segmento);
if (f.classificacao) partes.push(CLASSIFICACAO_LABELS[f.classificacao] || f.classificacao);
if (f.prioridade) partes.push(`P${f.prioridade}`);
if (f.produto) partes.push(`produto: ${f.produto}`);
if (f.interesse) partes.push(f.interesse);
if (f.telefone_valido) partes.push(f.telefone_valido === 'sim' ? 'com telefone' : 'sem telefone');
if (f.website) partes.push(f.website === 'sim' ? 'com website' : 'sem website');
if (f.email) partes.push(f.email === 'sim' ? 'com e-mail' : 'sem e-mail');
if (f.exportado) partes.push(f.exportado === 'nao' ? 'ainda não exportados' : 'já exportados');
if (f.q) partes.push(`busca "${f.q}"`);
return partes.length ? partes.join(' · ') : 'todos os leads';
}
// ---------- carregamento ----------
async function atualizar() {
page = 1;
const [summary, evolutionRes, oportunidadeRes] = await Promise.all([
API.get(`/api/dashboard/summary?${filtroQuery()}`),
API.get(`/api/dashboard/evolution?dias=14&${filtroQuery()}`),
API.get(`/api/dashboard/oportunidade?${filtroQuery()}`)
]);
await Promise.all([carregarTiles(summary), carregarRankings(summary), carregarFunil(summary), carregarTabela()]);
carregarEvolucaoDiaria(evolutionRes.evolution);
carregarMaiorOportunidade(oportunidadeRes.oportunidade);
}
function invalidar() {
carregado = false;
clearTimeout(refreshTimer);
refreshTimer = setTimeout(() => {
if (document.querySelector('#tab-tratamento.active')) ativar();
}, 800);
}
async function ativar() {
if (carregado) return;
carregado = true;
await Promise.all([carregarFacets(), atualizar(), carregarExports()]);
}
async function carregarFacets() {
const facets = await API.get('/api/leads/facets');
const fill = (select, valores, prefixo, formatar = (v) => v) => {
const atual = select.value;
const primeira = select.options[0];
select.replaceChildren(primeira, ...valores.map((item) =>
h('option', { value: item.value }, `${formatar(item.value)} (${fmt.format(item.total)})`)
));
select.value = atual;
};
fill(el('f-uf'), facets.ufs);
fill(el('f-interesse'), facets.interesses);
fill(el('f-produto'), facets.produtos);
const segSelect = el('f-segmento');
const atual = segSelect.value;
segSelect.replaceChildren(segSelect.options[0], ...facets.segmentos.map((s) =>
h('option', { value: s.value }, `${s.value.replace(/_/g, ' ')} (${fmt.format(s.total)})`)
));
segSelect.value = atual;
el('dl-cidades').replaceChildren(...facets.cidades.map((c) => h('option', { value: c.value })));
}
async function salvarMetaDiaria(valor) {
const meta = Math.max(1, Number(valor) || 100);
await API.put('/api/settings', { meta_diaria: meta });
const summary = await API.get(`/api/dashboard/summary?${filtroQuery()}`);
carregarTiles(summary);
}
async function carregarTiles(s) {
const pct = (n) => s.total ? ` (${((n / s.total) * 100).toFixed(1)}%)` : '';
el('stat-tiles').replaceChildren(
tile(s.total, 'Leads no filtro', ''),
tile(s.com_whatsapp, 'Com telefone', pct(s.com_whatsapp), 't-good'),
tile(s.p1, '🎯 Potencial Comercial Imediato', `${s.potencial_pct}% da base — atacar agora`, 't-good'),
tile(s.p4, '📦 P4 — campanhas específicas', pct(s.p4))
);
el('stat-tiles').append(
cardCobertura(s),
cardPrioridades(s),
cardMeta(s)
);
function tile(valor, rotulo, detalhe, cls = '') {
return h('div', { class: `tile ${cls}` },
h('div', { class: 'valor' }, fmt.format(valor)),
h('div', { class: 'rotulo' }, rotulo),
h('div', { class: 'detalhe' }, detalhe)
);
}
function cardCobertura(s) {
const cidadeAtiva = filtroAtual().cidade;
const titulo = cidadeAtiva ? `Cobertura em ${cidadeAtiva}` : 'Base Comercial — cobertura';
return h('div', { class: 'tile tile-wide' },
h('div', { class: 'rotulo' }, titulo),
h('div', { class: 'progress-bar' },
h('div', { class: 'progress-fill', style: `width:${s.cobertura_pct}%` })),
h('div', { class: 'detalhe' },
`${s.cobertura_pct}% trabalhada · ${fmt.format(s.exportados)} exportadas · ${fmt.format(s.disponiveis)} disponíveis`)
);
}
function cardPrioridades(s) {
const ativa = filtroAtual().prioridade;
const linhas = [
['P1', s.p1, s.p1_pct], ['P2', s.p2, s.p2_pct], ['P3', s.p3, s.p3_pct], ['P4', s.p4, s.p4_pct]
];
return h('div', { class: 'tile tile-wide' },
h('div', { class: 'rotulo' }, 'Distribuição por prioridade'),
h('div', { class: 'ranking', style: 'margin-top:6px' }, ...linhas.map(([label, total, p], i) =>
h('div', {
class: `rank-row${ativa === String(i + 1) ? ' ativo' : ''}`,
title: `${label}: ${fmt.format(total)} leads — clique para filtrar`,
onclick: () => toggleFiltro('f-prioridade', i + 1)
},
h('div', { class: 'rank-label' }, label),
h('div', { class: 'rank-track' }, h('div', { class: 'rank-fill', style: `width:${Math.max(1.5, p)}%` })),
h('div', { class: 'rank-value' }, `${p}%`)
)))
);
}
function cardMeta(s) {
return h('div', { class: 'tile tile-wide' },
h('div', { class: 'row space-between' },
h('div', { class: 'rotulo' }, '🎯 Meta diária'),
h('input', {
type: 'number', min: '1', value: String(s.meta_diaria), class: 'meta-input',
onchange: (e) => salvarMetaDiaria(e.target.value)
})
),
h('div', { class: 'progress-bar' },
h('div', { class: 'progress-fill', style: `width:${Math.min(100, s.meta_pct)}%` })),
h('div', { class: 'detalhe' },
`${fmt.format(s.hoje_exportados)} de ${fmt.format(s.meta_diaria)} hoje (${s.meta_pct}%) · ~${fmt.format(s.dias_estimados)} dias úteis restantes no ritmo atual`)
);
}
}
async function carregarRankings(summary) {
const cidadeAtiva = filtroAtual().cidade;
el('rank-produtos-titulo').textContent = cidadeAtiva ? `Produtos recomendados em ${cidadeAtiva}` : 'Top produto recomendado';
const [cidades, segmentos, produtos] = await Promise.all([
API.get(`/api/dashboard/rankings?by=cidade&limit=10&${filtroQuery()}`),
API.get(`/api/dashboard/rankings?by=segmento&limit=10&${filtroQuery()}`),
API.get(`/api/dashboard/rankings?by=produto&limit=10&${filtroQuery()}`)
]);
const f = filtroAtual();
const segmentoValuePorLabel = (label) => {
const opts = [...el('f-segmento').options];
const porValor = opts.find((o) => o.value && o.value.replace(/_/g, ' ').toLowerCase() === label.toLowerCase());
const porTexto = opts.find((o) => o.textContent.toLowerCase().startsWith(label.toLowerCase()));
return (porValor || porTexto)?.value || '';
};
renderRanking(el('rank-cidades'), cidades.ranking,
(label) => toggleFiltro('f-cidade', label),
null,
(label) => f.cidade === label);
renderRanking(el('rank-segmentos'), segmentos.ranking,
(label) => toggleFiltro('f-segmento', segmentoValuePorLabel(label)),
null,
(label) => !!f.segmento && f.segmento === segmentoValuePorLabel(label));
renderRanking(el('rank-produtos'), produtos.ranking,
(label) => toggleFiltro('f-produto', label),
summary.total,
(label) => f.produto === label);
carregarOportunidadesDoDia(produtos.ranking, summary.total);
}
function carregarOportunidadesDoDia(produtosRanking, totalGeral) {
const top3 = produtosRanking.slice(0, 3);
if (!top3.length) {
el('card-oportunidades-dia').replaceChildren(
h('h2', {}, '🔥 Oportunidades do Dia'),
h('div', { class: 'muted' }, 'Sem dados para este filtro.')
);
return;
}
const produtoAtivo = filtroAtual().produto;
el('card-oportunidades-dia').replaceChildren(
h('h2', {}, '🔥 Oportunidades do Dia'),
h('div', { class: 'ranking', style: 'margin-top:6px' }, ...top3.map((r) => {
const p = totalGeral ? Math.round((r.total / totalGeral) * 100) : 0;
return h('div', {
class: `rank-row${produtoAtivo === r.label ? ' ativo' : ''}`,
title: `${r.label}: ${fmt.format(r.total)} leads — clique para filtrar`,
onclick: () => toggleFiltro('f-produto', r.label)
},
h('div', { class: 'rank-label' }, r.label),
h('div', { class: 'rank-track' }, h('div', { class: 'rank-fill', style: `width:${Math.max(1.5, p)}%` })),
h('div', { class: 'rank-value' }, `${p}%`)
);
}))
);
}
function dataBrCurta(diaIso) {
const [, mes, dia] = diaIso.split('-');
return `${dia}/${mes}`;
}
function carregarEvolucaoDiaria(evolution) {
if (!evolution.length) {
el('card-evolucao-diaria').replaceChildren(
h('h2', {}, '📈 Evolução Diária'),
h('div', { class: 'muted' }, 'Nenhuma exportação registrada ainda.')
);
return;
}
const max = Math.max(...evolution.map((d) => d.total));
el('card-evolucao-diaria').replaceChildren(
h('h2', {}, '📈 Evolução Diária'),
h('div', { class: 'muted', style: 'margin:2px 0 8px' }, 'Exportações por dia — única atividade com data registrada hoje.'),
h('div', { class: 'ranking' }, ...evolution.map((d) => h('div', { class: 'rank-row nao-clicavel' },
h('div', { class: 'rank-label' }, dataBrCurta(d.dia)),
h('div', { class: 'rank-track' }, h('div', { class: 'rank-fill', style: `width:${Math.max(1.5, (d.total / max) * 100)}%` })),
h('div', { class: 'rank-value' }, fmt.format(d.total))
)))
);
}
function carregarMaiorOportunidade(oportunidade) {
if (!oportunidade) {
el('card-maior-oportunidade').replaceChildren(
h('h2', {}, '🏆 Maior Oportunidade'),
h('div', { class: 'muted' }, 'Sem dados suficientes para este filtro.')
);
return;
}
const f = filtroAtual();
const jaAplicado = f.cidade === oportunidade.cidade && f.produto === oportunidade.produto;
el('card-maior-oportunidade').replaceChildren(
h('h2', {}, '🏆 Maior Oportunidade'),
h('div', {
class: `row space-between oportunidade-clicavel${jaAplicado ? ' ativo' : ''}`,
style: 'margin-top:6px',
title: jaAplicado ? 'Clique para remover o filtro cidade + produto' : 'Clique para filtrar por esta cidade + produto',
onclick: () => {
el('f-cidade').value = jaAplicado ? '' : oportunidade.cidade;
el('f-produto').value = jaAplicado ? '' : oportunidade.produto;
atualizar();
}
},
h('div', {}, h('div', { class: 'rotulo' }, 'Cidade'), h('div', { class: 'valor', style: 'font-size:20px' }, oportunidade.cidade)),
h('div', {}, h('div', { class: 'rotulo' }, 'Produto'), h('div', { class: 'valor', style: 'font-size:20px' }, oportunidade.produto)),
h('div', {}, h('div', { class: 'rotulo' }, 'Empresas'), h('div', { class: 'valor', style: 'font-size:20px' }, fmt.format(oportunidade.total)))
)
);
}
function renderRanking(box, ranking, onClick, totalGeral, isAtivo = () => false) {
if (!ranking.length) {
box.replaceChildren(h('div', { class: 'muted' }, 'Sem dados para este filtro.'));
return;
}
const max = ranking[0].total;
box.replaceChildren(...ranking.map((r) => {
const pctGeral = totalGeral ? ` · ${Math.round((r.total / totalGeral) * 100)}%` : '';
const ativo = isAtivo(r.label);
return h('div', {
class: `rank-row${ativo ? ' ativo' : ''}`,
title: `${r.label}: ${fmt.format(r.total)} leads (${fmt.format(r.p1)} prioridade 1) — ${ativo ? 'clique para remover o filtro' : 'clique para filtrar'}`,
onclick: () => onClick(r.label)
},
h('div', { class: 'rank-label' }, r.label),
h('div', { class: 'rank-track' },
h('div', { class: 'rank-fill', style: `width:${Math.max(1.5, (r.total / max) * 100)}%` })),
h('div', { class: 'rank-value' }, `${fmt.format(r.total)}${pctGeral}`)
);
}));
}
const FUNIL_ETAPAS = [
{ key: 'total', label: 'Leads capturados' },
{ key: 'disponiveis', label: 'Leads disponíveis' },
{ key: 'exportados', label: 'Leads exportados' },
{ key: null, label: 'Empresas contatadas' },
{ key: null, label: 'Empresas interessadas' },
{ key: null, label: 'Propostas enviadas' },
{ key: null, label: 'Clientes convertidos' }
];
function carregarFunil(s) {
const base = s.total || 1;
el('card-funil').replaceChildren(
h('h2', {}, 'Evolução Comercial'),
h('div', { class: 'funil' }, ...FUNIL_ETAPAS.map((etapa) => {
const pendente = etapa.key === null;
const valor = pendente ? null : s[etapa.key];
const largura = pendente ? 0 : Math.max(1.5, (valor / base) * 100);
return h('div', { class: `funil-etapa${pendente ? ' pendente' : ''}`,
title: pendente ? 'Aguardando integração com CRM' : '' },
h('div', { class: 'funil-label' }, etapa.label),
h('div', { class: 'funil-barra' }, h('div', { class: 'funil-fill', style: `width:${largura}%` })),
h('div', { class: 'funil-valor' }, pendente ? '—' : fmt.format(valor))
);
}))
);
}
async function carregarTabela() {
const data = await API.get(`/api/leads?${filtroQuery({ page, pageSize: 50 })}`);
totalPages = Math.max(1, Math.ceil(data.total / data.pageSize));
el('leads-total').textContent = `— ${fmt.format(data.total)} encontrados`;
el('pg-info').textContent = `página ${data.page} de ${fmt.format(totalPages)}`;
el('pg-prev').disabled = page <= 1;
el('pg-next').disabled = page >= totalPages;
const tbody = document.querySelector('#tabela-leads tbody');
if (!data.rows.length) {
tbody.replaceChildren(h('tr', {}, h('td', { colspan: '11', class: 'muted' }, 'Nenhum lead com este filtro.')));
return;
}
const abrirWhatsAppLead = async (event, lead) => {
event.stopPropagation();
if (!lead.whatsapp) {
toast('Este lead não tem telefone cadastrado');
return;
}
let leadCompleto = lead;
await comCarregando(event.currentTarget, async () => {
try {
leadCompleto = await API.get(`/api/leads/${lead.id}`);
} catch {
leadCompleto = lead;
}
});
const settingsButton = el('btn-settings');
settingsButton.click();
setTimeout(() => {
window.MaquinaSelecionarLeadManual?.(leadCompleto);
el('manual-numero').value = leadCompleto.whatsapp || lead.whatsapp;
const empresa = leadCompleto.empresa || lead.empresa;
const cidade = leadCompleto.cidade || lead.cidade;
el('manual-mensagem').value = `Olá! Encontrei a ${empresa}${cidade ? ` em ${cidade}` : ''} e gostaria de apresentar meu serviço. Podemos conversar?`;
el('manual-status').textContent = '';
}, 250);
};
tbody.replaceChildren(...data.rows.map((lead) => h('tr', { onclick: () => abrirLead(lead.id) },
h('td', { class: 'acao-col' }, h('button', {
class: 'whatsapp-action',
title: 'Enviar WhatsApp para este lead',
onclick: (event) => abrirWhatsAppLead(event, lead)
}, '☎')),
h('td', { class: 'empresa-cell', title: lead.legal_name || lead.empresa }, lead.empresa),
h('td', {}, lead.whatsapp || '—'),
h('td', {}, whatsappStatusCell(lead)),
h('td', {}, lead.cidade),
h('td', {}, lead.segmento_label || lead.segmento),
h('td', {}, h('span', { class: `badge prioridade-${lead.prioridade}` }, `P${lead.prioridade}`)),
h('td', { class: 'num' }, String(lead.score)),
h('td', {}, h('span', { class: `badge ${lead.classificacao}` }, CLASSIFICACAO_LABELS[lead.classificacao] || lead.classificacao)),
h('td', { class: 'produto-cell' }, produtoCell(lead.produto_principal, lead.afinidade)),
h('td', {}, lead.exported_at ? '📤 sim' : '—')
)));
}
const AFINIDADE_TABELA_LABELS = { alta: 'Alta aderência', media: 'Média aderência', baixa: 'Baixa aderência' };
function whatsappCheckedAt(value) {
if (!value) return '';
const normalized = value.includes('T') ? value : `${value.replace(' ', 'T')}Z`;
return Number.isNaN(Date.parse(normalized)) ? '' : dataBr(value);
}
function whatsappStatusCell(lead) {
const status = lead.whatsapp_status || 'nao_consultado';
const detalhes = [];
if (lead.whatsapp_numero_resolvido) detalhes.push(`Número confirmado: ${lead.whatsapp_numero_resolvido}`);
const checkedAt = whatsappCheckedAt(lead.whatsapp_checked_at);
if (checkedAt) detalhes.push(`Consultado em ${checkedAt}`);
return h('div', { class: 'whatsapp-verification' },
h('span', { class: `badge whatsapp-${status}` }, WHATSAPP_STATUS_LABELS[status] || status),
numeroCompartilhadoBadge(lead),
detalhes.length ? h('div', { class: 'sub' }, detalhes.join(' · ')) : null);
}
function numeroCompartilhadoBadge(lead) {
if (!lead.numero_compartilhado) return null;
const classe = lead.numero_compartilhado_liberado ? 'numero-compartilhado-liberado' : 'numero-compartilhado';
const texto = lead.numero_compartilhado_liberado
? `Compartilhado (liberado, ${lead.numero_compartilhado_total} leads)`
: `Número compartilhado (${lead.numero_compartilhado_total} leads)`;
return h('span', { class: `badge ${classe}` }, texto);
}
function produtoCell(produto, afinidade) {
if (!produto) return '—';
const label = AFINIDADE_TABELA_LABELS[afinidade];
return label ? `${produto} · ${label}` : produto;
}
const OBSERVACAO_POR_PRIORIDADE = {
1: 'Empresa apta para abordagem imediata.',
2: 'Aguardar a janela de trabalho desta semana.',
3: 'Abordar quando não houver leads de prioridade mais alta.',
4: 'Reservar para campanhas específicas — baixa prioridade comercial.'
};
const AFINIDADE_LABELS = { alta: 'nicho de alta aderência', media: 'nicho de média aderência', baixa: 'nicho de baixa aderência' };
function montarResumoComercial(lead) {
const potencial = Math.max(1, Math.min(5, Math.round((lead.score_comercial || 0) / 20)));
const pontosFortes = [];
if (lead.telefone_valido) pontosFortes.push('Telefone válido');
if (lead.cnpj_valido) pontosFortes.push('CNPJ válido');
if (lead.website) pontosFortes.push('Website');
if (lead.email) pontosFortes.push('E-mail cadastrado');
if (lead.afinidade) pontosFortes.push(AFINIDADE_LABELS[lead.afinidade] || 'nicho identificado');
return h('div', { class: 'resumo-comercial' },
h('h3', {}, 'Resumo comercial'),
h('div', { class: 'row space-between' },
h('span', { class: 'estrelas', title: `Potencial comercial: ${potencial}/5` }, estrelas(potencial)),
h('span', { class: `badge prioridade-${lead.prioridade}` }, PRIORIDADE_LABELS[lead.prioridade] || `P${lead.prioridade}`)
),
lead.produtos && lead.produtos.length
? h('div', {}, h('div', { class: 'muted', style: 'margin:8px 0 4px' }, 'Produtos recomendados'),
...lead.produtos.map((p) => h('div', {}, h('span', { class: 'estrelas' }, estrelas(p.estrelas)), ` ${p.nome}`)))
: h('div', { class: 'muted', style: 'margin-top:8px' }, 'Nenhum produto sugerido para este nicho.'),
pontosFortes.length
? h('ul', { class: 'pontos-fortes', style: 'margin-top:10px' }, ...pontosFortes.map((p) => h('li', {}, `✓ ${p}`)))
: null,
h('p', { class: 'muted', style: 'margin:10px 0 0' }, OBSERVACAO_POR_PRIORIDADE[lead.prioridade] || '')
);
}
function montarScoreBreakdown(lead) {
const nichoPontos = lead.afinidade === 'alta' ? '+40' : lead.afinidade === 'baixa' ? '+20' : '+30';
const qualidade = [
[lead.telefone_valido, 'Telefone válido', '+30'],
[lead.cnpj_valido, 'CNPJ válido', '+20'],
[lead.empresa_valida, 'Empresa válida', '+20'],
[!!lead.cidade, 'Cidade identificada', '+5'],
[!!lead.segmento, 'Nicho identificado', '+10'],
[!!lead.cnae, 'CNAE identificado', '+10'],
[!!lead.website, 'Website informado', '+5']
];
const comercial = [
[true, `Nicho ${AFINIDADE_TABELA_LABELS[lead.afinidade] || 'de aderência não identificada'}`, nichoPontos],
[!!lead.website, 'Website informado', '+15'],
[!!lead.email, 'E-mail informado', '+10'],
[lead.telefone_valido, 'Telefone válido', '+15'],
[lead.cnpj_valido, 'CNPJ válido', '+20']
];
const bloco = (itens, titulo, valorTotal) => h('div', {},
h('div', { class: 'row space-between', style: 'margin-top:10px' },
h('strong', {}, titulo), h('span', {}, String(valorTotal))),
h('ul', { class: 'score-breakdown' }, ...itens.map(([ok, label, pontos]) =>
h('li', { class: ok ? 'ok' : 'nok' }, `${ok ? '✓' : '✗'} ${label} (${pontos})`)))
);
return h('div', { class: 'score-breakdown-wrap' },
bloco(qualidade, 'Score Qualidade', lead.score_qualidade),
bloco(comercial, 'Score Comercial', lead.score_comercial)
);
}
async function abrirLead(id) {
const lead = await API.get(`/api/leads/${id}`);
const linha = (dt, dd) => [h('dt', {}, dt), h('dd', {}, dd || '—')];
el('lead-detalhe').replaceChildren(
h('h2', {}, lead.empresa),
montarResumoComercial(lead),
montarScoreBreakdown(lead),
h('dl', { class: 'detalhe-grid', style: 'margin-top:14px' },
linha('Razão social', lead.legal_name),
linha('Telefone', lead.whatsapp),
linha('Status WhatsApp', WHATSAPP_STATUS_LABELS[lead.whatsapp_status] || lead.whatsapp_status || 'Não consultado'),
linha('Número WhatsApp confirmado', lead.whatsapp_numero_resolvido),
linha('WhatsApp consultado em', whatsappCheckedAt(lead.whatsapp_checked_at)),
linha('Número compartilhado com outro lead', lead.numero_compartilhado
? `${lead.numero_compartilhado_total} leads${lead.numero_compartilhado_liberado ? ' (liberado)' : ''}`
: 'Não'),
linha('Cidade/UF', `${lead.cidade}/${lead.uf}`),
linha('Endereço', lead.endereco),
linha('Nicho', lead.segmento_label || lead.segmento),
linha('Interesse', lead.interesse),
linha('CNPJ', lead.cnpj),
linha('CNAE', lead.cnae),
linha('Score geral', `${lead.score} (${CLASSIFICACAO_LABELS[lead.classificacao] || lead.classificacao})`),
linha('Dores', (lead.pains || []).join(', ')),
linha('Origem', lead.origem),
linha('Capturado em', dataBr(lead.captured_at || lead.created_at)),
linha('Exportado em', lead.exported_at ? dataBr(lead.exported_at) : 'ainda não'),
linha('Exportações', lead.exports.length
? lead.exports.map((e) => `#${e.id} ${e.tipo}`).join(', ')
: 'nenhuma')
),
h('div', { class: 'row space-between', style: 'margin-top:14px' },
h('button', { class: 'btn-ghost', onclick: () => el('modal-lead').close() }, 'Fechar')
)
);
el('modal-lead').showModal();
}
// ---------- exportação ----------
const EXPORT_ATALHOS = [
{ label: 'Só P1', set: () => { el('f-prioridade').value = '1'; } },
{ label: 'Não exportados', set: () => { el('f-exportado').value = 'nao'; } }
];
async function atualizarRecapExport() {
const { total } = await API.get(`/api/dashboard/summary?${filtroQuery()}`);
el('export-recap').textContent = `${fmt.format(total)} leads · ${descreverFiltro()}`;
return total;
}
function renderAtalhosExport() {
el('export-atalhos').replaceChildren(...EXPORT_ATALHOS.map((atalho) => {
const botao = h('button', {
type: 'button', class: 'chip', onclick: async () => {
await comCarregando(botao, async () => {
atalho.set();
await atualizar();
await atualizarRecapExport();
});
}
}, atalho.label);
return botao;
}));
}
async function abrirModalExport(event) {
const botao = event?.currentTarget || el('btn-exportar');
await comCarregando(botao, async () => {
const total = await atualizarRecapExport();
if (!total) {
toast('Nenhum lead com este filtro — ajuste os filtros antes de exportar');
return;
}
renderAtalhosExport();
el('modal-export').showModal();
});
}
async function confirmarExport(event) {
event.preventDefault();
const tipo = document.querySelector('input[name="export-tipo"]:checked').value;
const body = {
tipo,
filtro: filtroAtual(),
params: { marcarExportado: el('export-marcar').checked }
};
try {
const { jobId, totalEstimado } = await comCarregando(el('btn-confirmar-export'), () => API.post('/api/exports', body));
el('modal-export').close();
toast(`Gerando arquivos para ${fmt.format(totalEstimado)} leads...`);
API.watchJob(jobId, {
done() {
toast('✅ Exportação pronta! Baixe os arquivos na lista abaixo.');
carregarExports();
atualizar();
},
error(e) {
toast(`❌ Erro na exportação: ${e.message}`);
carregarExports();
}
});
} catch (error) {
toast(`Erro: ${error.message}`);
}
}
async function carregarExports() {
const { exports } = await API.get('/api/exports?limit=12');
const box = el('historico-exports');
if (!exports.length) {
box.replaceChildren(h('div', { class: 'muted' }, 'Nenhuma exportação gerada ainda.'));
return;
}
const tipoLabel = { excel: 'Planilha CSV + XLS', legado: 'Importação antiga' };
box.replaceChildren(...exports.map((e) => h('div', { class: 'item' },
h('div', {},
h('div', {}, h('strong', {}, `#${e.id} · ${tipoLabel[e.tipo] || e.tipo}`),
` · ${fmt.format(e.total_leads)} leads`),
h('div', { class: 'sub' }, `${dataBr(e.created_at)} · ${e.status}${e.error ? ` · ${e.error.slice(0, 60)}` : ''}`)
),
h('div', { class: 'row' }, ...(e.status === 'done' && e.tipo !== 'legado'
? e.arquivos.map((nome) =>
h('a', { class: 'btn-ghost', href: `/api/exports/${e.id}/download?arquivo=${encodeURIComponent(nome)}` },
`⬇ ${nome.split('.').pop().toUpperCase()}`))
: []))
)));
}
// ---------- eventos ----------
function bindEvents() {
let debounce = null;
for (const input of document.querySelectorAll('#filtros [data-filtro]')) {
input.addEventListener(input.tagName === 'SELECT' ? 'change' : 'input', () => {
clearTimeout(debounce);
debounce = setTimeout(atualizar, input.tagName === 'SELECT' ? 0 : 350);
});
}
el('btn-limpar-filtros').addEventListener('click', (event) => {
comCarregando(event.currentTarget, async () => {
for (const input of document.querySelectorAll('#filtros [data-filtro]')) input.value = '';
await atualizar();
});
});
// pg-prev/pg-next: nao usam comCarregando de proposito. carregarTabela() ja
// controla o atributo disabled destes botoes conforme a pagina atual
// (limite inicio/fim); o finally do comCarregando reabilitaria o botao
// incondicionalmente ao final, conflitando com esse controle de borda.
el('pg-prev').addEventListener('click', () => { page -= 1; carregarTabela(); });
el('pg-next').addEventListener('click', () => { page += 1; carregarTabela(); });
el('btn-exportar').addEventListener('click', abrirModalExport);
el('form-export').addEventListener('submit', confirmarExport);
}
return { ativar, invalidar, bindEvents };
})();
window.Tratamento = Tratamento;