Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: gti-maquina-comercial git-subtree-mainline:93ca82979egit-subtree-split:b77484946f
254 lines
11 KiB
JavaScript
254 lines
11 KiB
JavaScript
/* 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 (<dialog> 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 };
|
|
})();
|