Add campaign detail panel: queue approval, health and variant stats
This commit is contained in:
@@ -236,6 +236,43 @@
|
||||
</div>
|
||||
<div id="campanhas-lista" class="lista"></div>
|
||||
|
||||
<div id="campanha-detalhe" class="card hidden">
|
||||
<div class="row space-between">
|
||||
<h2 id="detalhe-nome"></h2>
|
||||
<div class="row">
|
||||
<span class="chip" id="detalhe-status"></span>
|
||||
<button class="btn-ghost btn-sm" id="btn-fechar-detalhe" type="button">Fechar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" id="detalhe-acoes">
|
||||
<button class="btn-ghost btn-sm" id="btn-iniciar-campanha" type="button">Iniciar</button>
|
||||
<button class="btn-ghost btn-sm" id="btn-pausar-campanha" type="button">Pausar</button>
|
||||
<button class="btn-ghost btn-sm" id="btn-retomar-campanha" type="button">Retomar</button>
|
||||
<button class="btn-ghost btn-sm" id="btn-gerar-fila" type="button">Gerar fila</button>
|
||||
</div>
|
||||
<p class="muted" id="detalhe-saude"></p>
|
||||
<div id="detalhe-variantes" class="row"></div>
|
||||
|
||||
<div class="row space-between">
|
||||
<h3>Fila</h3>
|
||||
<select id="fila-filtro-status">
|
||||
<option value="preview">Preview</option>
|
||||
<option value="approved">Aprovadas</option>
|
||||
<option value="sent">Enviadas</option>
|
||||
<option value="error">Erro</option>
|
||||
<option value="cancelled">Canceladas</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="btn-ghost btn-sm" id="btn-aprovar-selecionados" type="button">Aprovar selecionados</button>
|
||||
<button class="btn-ghost btn-sm" id="btn-cancelar-selecionados" type="button">Cancelar selecionados</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th></th><th>Empresa</th><th>Numero</th><th>Mensagem</th><th>Status</th></tr></thead>
|
||||
<tbody id="fila-tabela"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<dialog id="modal-nova-campanha">
|
||||
<form id="form-nova-campanha" method="dialog">
|
||||
<h2>Nova campanha</h2>
|
||||
|
||||
+90
-1
@@ -58,7 +58,7 @@ const Campanhas = (() => {
|
||||
}
|
||||
|
||||
function renderCampanhaCard(campanha) {
|
||||
return h('div', { class: 'item' },
|
||||
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(', ')}`)),
|
||||
@@ -104,8 +104,97 @@ const Campanhas = (() => {
|
||||
}
|
||||
});
|
||||
|
||||
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)' : ''}`)
|
||||
));
|
||||
}
|
||||
|
||||
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) =>
|
||||
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))
|
||||
));
|
||||
}
|
||||
|
||||
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 () => {
|
||||
await API.post(`/api/campaigns/${campanhaAtualId}/iniciar`, {});
|
||||
await carregarDetalhe(campanhaAtualId);
|
||||
await carregarLista();
|
||||
});
|
||||
el('btn-pausar-campanha').addEventListener('click', async () => {
|
||||
await API.post(`/api/campaigns/${campanhaAtualId}/pausar`, { motivo: 'manual' });
|
||||
await carregarDetalhe(campanhaAtualId);
|
||||
await carregarLista();
|
||||
});
|
||||
el('btn-retomar-campanha').addEventListener('click', async () => {
|
||||
await API.post(`/api/campaigns/${campanhaAtualId}/retomar`, {});
|
||||
await carregarDetalhe(campanhaAtualId);
|
||||
await carregarLista();
|
||||
});
|
||||
|
||||
el('btn-aprovar-selecionados').addEventListener('click', async () => {
|
||||
const queueIds = filaSelecionada();
|
||||
if (!queueIds.length) return;
|
||||
const resultado = await API.post(`/api/campaigns/${campanhaAtualId}/fila/aprovar`, { queueIds });
|
||||
toast(`${resultado.aprovados} aprovados`);
|
||||
await carregarFila(campanhaAtualId);
|
||||
});
|
||||
el('btn-cancelar-selecionados').addEventListener('click', async () => {
|
||||
const queueIds = filaSelecionada();
|
||||
if (!queueIds.length) return;
|
||||
const resultado = await API.post(`/api/campaigns/${campanhaAtualId}/fila/cancelar`, { queueIds, motivo: 'operador' });
|
||||
toast(`${resultado.cancelados} cancelados`);
|
||||
await carregarFila(campanhaAtualId);
|
||||
});
|
||||
}
|
||||
|
||||
return { ativar };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user