/* Aba Campanhas: criacao, fila de aprovacao, saude/variantes, estagio/agendamento. */ const Campanhas = (() => { let carregado = false; let variantesCount = 0; const STATUS_LABELS = { rascunho: 'Rascunho', rodando: 'Rodando', pausado: 'Pausado', concluido: 'Concluido' }; const MAX_VARIANTES = 3; // A/B/C — mesmo limite usado na convencao do restante da UI/backend. function addVarianteRow(codigoSugerido) { if (variantesCount >= MAX_VARIANTES) return; const codigo = codigoSugerido || String.fromCharCode(65 + variantesCount); variantesCount += 1; el('campanha-variantes').append( h('div', { class: 'row', 'data-variante-code': codigo }, h('span', { class: 'chip' }, codigo), h('textarea', { class: 'campanha-variante-mensagem', rows: 2, placeholder: 'Ola {empresa}, ...' })) ); atualizarBotaoAddVariante(); } function atualizarBotaoAddVariante() { el('btn-add-variante').disabled = variantesCount >= MAX_VARIANTES; } async function carregarProdutos() { const { produtos } = await API.get('/api/campaigns/produtos'); el('campanha-produto').replaceChildren( ...produtos.map((p) => h('option', { value: p.value }, `${p.value} (${fmt.format(p.total)} leads)`)) ); } async function carregarSegmentos(produto) { const container = el('campanha-segmentos'); if (!produto) { container.replaceChildren(); return; } const { segmentos } = await API.get(`/api/campaigns/segmentos?produto=${encodeURIComponent(produto)}`); container.replaceChildren(...segmentos.map((s) => h('label', { class: 'segmento-opcao' }, h('input', { type: 'checkbox', value: s.value, class: 'campanha-segmento-check' }), ` ${s.value} (${fmt.format(s.total)})`) )); } function segmentosSelecionados() { return Array.from(document.querySelectorAll('.campanha-segmento-check:checked')).map((i) => i.value); } function variantesPreenchidas() { return Array.from(el('campanha-variantes').children).map((row) => ({ code: row.dataset.varianteCode, mensagem: row.querySelector('.campanha-variante-mensagem').value.trim() })).filter((v) => v.mensagem); } async function abrirModalNovaCampanha() { el('campanha-nome').value = ''; el('campanha-limite-inicial').value = 10; el('campanha-limite-teto').value = 80; el('campanha-erro').textContent = ''; el('campanha-variantes').replaceChildren(); variantesCount = 0; addVarianteRow('A'); await carregarProdutos(); await carregarSegmentos(el('campanha-produto').value); el('modal-nova-campanha').showModal(); } function renderCampanhaCard(campanha) { return h('div', { class: 'item', onclick: () => carregarDetalhe(campanha.id).catch((e) => toast(`Erro: ${e.message}`)) }, h('div', {}, h('strong', {}, campanha.nome), h('span', { class: 'sub' }, ` ${campanha.produto} — ${campanha.segmentos.join(', ')}`)), h('span', { class: `chip ${campanha.status === 'rodando' ? 'ok' : campanha.status === 'pausado' ? 'warn' : ''}` }, STATUS_LABELS[campanha.status] || campanha.status)); } async function carregarLista() { const { campanhas } = await API.get('/api/campaigns'); el('campanhas-lista').replaceChildren( ...(campanhas.length ? campanhas.map(renderCampanhaCard) : [h('p', { class: 'muted' }, 'Nenhuma campanha criada ainda.')]) ); } function ativar() { if (carregado) return; carregado = true; el('btn-nova-campanha').addEventListener('click', () => { abrirModalNovaCampanha().catch((error) => toast(`Erro: ${error.message}`)); }); el('btn-cancelar-campanha').addEventListener('click', () => el('modal-nova-campanha').close()); el('btn-add-variante').addEventListener('click', () => addVarianteRow()); el('campanha-produto').addEventListener('change', (event) => { // O modal de nova campanha esta aberto ( via showModal()) quando // isto dispara — toast sozinho fica escondido atras do dialog (ver // comentario em api.js), entao o erro tambem precisa aparecer no // paragrafo inline #campanha-erro que ja existe pro erro de submit. carregarSegmentos(event.target.value).catch((error) => { toast(`Erro: ${error.message}`); el('campanha-erro').textContent = `Erro: ${error.message}`; }); }); el('btn-salvar-campanha').addEventListener('click', async () => { try { await API.post('/api/campaigns', { nome: el('campanha-nome').value.trim(), produto: el('campanha-produto').value, segmentos: segmentosSelecionados(), variantes: variantesPreenchidas(), limiteDiarioInicial: Number(el('campanha-limite-inicial').value) || 10, limiteDiarioTeto: Number(el('campanha-limite-teto').value) || 80 }); el('modal-nova-campanha').close(); toast('Campanha criada'); await carregarLista(); } catch (error) { el('campanha-erro').textContent = error.message; } }); ligarDetalheCampanha(); carregarLista().catch((error) => toast(`Erro: ${error.message}`)); } let campanhaAtualId = null; async function carregarDetalhe(campanhaId) { campanhaAtualId = campanhaId; const campanha = await API.get(`/api/campaigns/${campanhaId}`); el('detalhe-nome').textContent = campanha.nome; el('detalhe-status').textContent = STATUS_LABELS[campanha.status] || campanha.status; el('campanha-detalhe').classList.remove('hidden'); await Promise.all([carregarSaude(campanhaId), carregarVariantesStats(campanhaId), carregarFila(campanhaId)]); } async function carregarSaude(campanhaId) { const saude = await API.get(`/api/campaigns/${campanhaId}/saude`); el('detalhe-saude').textContent = saude.pause ? `Pausada automaticamente: ${saude.reason}` : 'Saude: sem alertas.'; } async function carregarVariantesStats(campanhaId) { const { variantes } = await API.get(`/api/campaigns/${campanhaId}/variantes-stats`); el('detalhe-variantes').replaceChildren(...variantes.map((v) => h('span', { class: 'chip' }, `${v.code}: ${v.enviados} enviados, ${(v.taxaResposta * 100).toFixed(1)}% resposta${v.elegivelParaComparacao ? ' (elegivel)' : ''}`) )); } const STAGE_OPTIONS = [ 'respondeu', 'interessado', 'demonstracao_agendada', 'sem_interesse', 'opt_out', 'venda_fechada' ]; async function carregarFila(campanhaId) { const status = el('fila-filtro-status').value; const { fila } = await API.get(`/api/campaigns/${campanhaId}/fila?status=${status}`); el('fila-tabela').replaceChildren(...fila.map((linha) => { const podeMarcarEstagio = linha.status === 'sent' || linha.status === 'error'; const stageSelect = h('select', { class: 'fila-estagio-select', disabled: podeMarcarEstagio ? null : 'disabled' }, h('option', { value: '' }, '—'), ...STAGE_OPTIONS.map((s) => h('option', { value: s }, s))); const marcarBtn = h('button', { class: 'btn-ghost btn-sm', type: 'button', disabled: podeMarcarEstagio ? null : 'disabled', onclick: async () => { try { if (!stageSelect.value) return; await API.post(`/api/campaigns/leads/${linha.lead_id}/estagio`, { stage: stageSelect.value, campaignId: campanhaId }); toast('Estagio atualizado'); if (stageSelect.value === 'demonstracao_agendada') { const dataAgendamento = prompt('Data/hora do agendamento (AAAA-MM-DD HH:MM):'); if (dataAgendamento) { await API.post(`/api/campaigns/leads/${linha.lead_id}/agendamentos`, { kind: 'demonstracao', scheduledAt: dataAgendamento }); toast('Agendamento registrado'); } } } catch (error) { toast(`Erro: ${error.message}`); } } }, 'Marcar'); return h('tr', {}, h('td', {}, h('input', { type: 'checkbox', class: 'fila-check', value: linha.id })), h('td', {}, String(linha.lead_id)), h('td', {}, linha.numero), h('td', {}, linha.rendered_message), h('td', {}, linha.status), h('td', { class: 'row' }, stageSelect, marcarBtn)); })); } function filaSelecionada() { return Array.from(document.querySelectorAll('.fila-check:checked')).map((i) => Number(i.value)); } function ligarDetalheCampanha() { el('btn-fechar-detalhe').addEventListener('click', () => el('campanha-detalhe').classList.add('hidden')); el('fila-filtro-status').addEventListener('change', () => carregarFila(campanhaAtualId).catch((e) => toast(`Erro: ${e.message}`))); el('btn-gerar-fila').addEventListener('click', async () => { try { const resultado = await API.post(`/api/campaigns/${campanhaAtualId}/gerar-fila`, {}); toast(`${resultado.adicionadosNaFila} leads adicionados a fila`); await carregarFila(campanhaAtualId); } catch (error) { toast(`Erro: ${error.message}`); } }); el('btn-iniciar-campanha').addEventListener('click', async () => { try { await API.post(`/api/campaigns/${campanhaAtualId}/iniciar`, {}); await carregarDetalhe(campanhaAtualId); await carregarLista(); } catch (error) { toast(`Erro: ${error.message}`); } }); el('btn-pausar-campanha').addEventListener('click', async () => { try { await API.post(`/api/campaigns/${campanhaAtualId}/pausar`, { motivo: 'manual' }); await carregarDetalhe(campanhaAtualId); await carregarLista(); } catch (error) { toast(`Erro: ${error.message}`); } }); el('btn-retomar-campanha').addEventListener('click', async () => { try { await API.post(`/api/campaigns/${campanhaAtualId}/retomar`, {}); await carregarDetalhe(campanhaAtualId); await carregarLista(); } catch (error) { toast(`Erro: ${error.message}`); } }); el('btn-aprovar-selecionados').addEventListener('click', async () => { const queueIds = filaSelecionada(); if (!queueIds.length) return; try { const resultado = await API.post(`/api/campaigns/${campanhaAtualId}/fila/aprovar`, { queueIds }); toast(`${resultado.aprovados} aprovados`); await carregarFila(campanhaAtualId); } catch (error) { toast(`Erro: ${error.message}`); } }); el('btn-cancelar-selecionados').addEventListener('click', async () => { const queueIds = filaSelecionada(); if (!queueIds.length) return; try { const resultado = await API.post(`/api/campaigns/${campanhaAtualId}/fila/cancelar`, { queueIds, motivo: 'operador' }); toast(`${resultado.cancelados} cancelados`); await carregarFila(campanhaAtualId); } catch (error) { toast(`Erro: ${error.message}`); } }); } return { ativar }; })();