Add Campanhas tab: listing and campaign creation
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
<nav class="tabs" role="tablist">
|
||||
<button class="tab active" data-tab="captura" role="tab" aria-selected="true">📡 Capturar Leads</button>
|
||||
<button class="tab" data-tab="tratamento" role="tab" aria-selected="false">📊 Tratar Leads</button>
|
||||
<button class="tab" data-tab="campanhas" role="tab" aria-selected="false">🎯 Campanhas</button>
|
||||
</nav>
|
||||
<span class="muted" id="usuario-logado"></span>
|
||||
<button class="btn-ghost hidden" id="btn-usuarios" title="Usuários">👤 Usuários</button>
|
||||
@@ -226,6 +227,48 @@
|
||||
<div id="historico-exports" class="lista"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ==================== CAMPANHAS ==================== -->
|
||||
<section id="tab-campanhas" class="tab-panel">
|
||||
<div class="row space-between">
|
||||
<h2>Campanhas de outreach</h2>
|
||||
<button class="btn-ghost btn-sm" id="btn-nova-campanha" type="button">+ Nova campanha</button>
|
||||
</div>
|
||||
<div id="campanhas-lista" class="lista"></div>
|
||||
|
||||
<dialog id="modal-nova-campanha">
|
||||
<form id="form-nova-campanha" method="dialog">
|
||||
<h2>Nova campanha</h2>
|
||||
<div class="field">
|
||||
<label for="campanha-nome">Nome</label>
|
||||
<input type="text" id="campanha-nome" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="campanha-produto">Produto</label>
|
||||
<select id="campanha-produto" required></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Segmentos</label>
|
||||
<div id="campanha-segmentos" class="chips"></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Variantes de mensagem (A/B/C)</label>
|
||||
<div id="campanha-variantes"></div>
|
||||
<button class="btn-ghost btn-sm" id="btn-add-variante" type="button">+ Variante</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Limite diário inicial <input type="number" id="campanha-limite-inicial" value="10" min="1"></label>
|
||||
<label>Teto do limite <input type="number" id="campanha-limite-teto" value="80" min="1"></label>
|
||||
</div>
|
||||
<p class="muted" id="campanha-erro"></p>
|
||||
<div class="row space-between">
|
||||
<button class="btn-ghost" id="btn-cancelar-campanha" type="button">Cancelar</button>
|
||||
<button class="btn btn-primary" id="btn-salvar-campanha" type="button">Criar campanha</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Modal de exportação -->
|
||||
@@ -480,6 +523,7 @@
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/captura.js"></script>
|
||||
<script src="/js/tratamento.js"></script>
|
||||
<script src="/js/campanhas.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
panel.classList.toggle('active', panel.id === `tab-${tab.dataset.tab}`);
|
||||
}
|
||||
if (tab.dataset.tab === 'tratamento') Tratamento.ativar();
|
||||
if (tab.dataset.tab === 'campanhas') Campanhas.ativar();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/* 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' };
|
||||
|
||||
function addVarianteRow(codigoSugerido) {
|
||||
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}, ...' }))
|
||||
);
|
||||
}
|
||||
|
||||
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: 'chip' },
|
||||
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' },
|
||||
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) => {
|
||||
carregarSegmentos(event.target.value).catch((error) => toast(`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;
|
||||
}
|
||||
});
|
||||
|
||||
carregarLista().catch((error) => toast(`Erro: ${error.message}`));
|
||||
}
|
||||
|
||||
return { ativar };
|
||||
})();
|
||||
Reference in New Issue
Block a user