Files
COMERCIAL-GTI/public/js/app.js
T
Gustavo 4f736562f3 Carry over pre-existing WABA outreach automation WIP
This work already existed uncommitted in the main working tree (not
authored by this branch) and is required as a runtime dependency for the
campaign outreach port (zpro-client, templates, outreach routes/UI). Copied
in as-is, untouched, so this feature branch has a working baseline; the
original uncommitted state in the main checkout is left exactly as it was.
2026-08-01 10:12:25 -03:00

685 lines
30 KiB
JavaScript

/* Shell do aplicativo: login + abas + configurações + usuários. */
(async function init() {
// sessao (o servidor ja bloqueia "/" sem cookie, mas precisamos dos dados do usuario)
let usuarioAtual;
try {
({ user: usuarioAtual } = await API.get('/api/auth/me'));
} catch {
window.location.href = '/login.html';
return;
}
el('usuario-logado').textContent = `${usuarioAtual.username} (${usuarioAtual.role === 'admin' ? 'admin' : 'comercial'})`;
el('btn-usuarios').classList.toggle('hidden', usuarioAtual.role !== 'admin');
el('btn-sair').addEventListener('click', async () => {
await API.post('/api/auth/logout');
window.location.href = '/login.html';
});
// usuarios (somente admin)
async function carregarListaUsuarios() {
const { users } = await API.get('/api/users');
el('lista-usuarios').replaceChildren(...users.map((u) =>
h('div', { class: 'row space-between' },
h('span', {}, u.username),
h('span', { class: 'muted' }, u.role === 'admin' ? 'Administrador' : 'Comercial'))));
}
el('btn-usuarios').addEventListener('click', async () => {
el('modal-usuarios').showModal();
try {
await carregarListaUsuarios();
} catch (error) {
toast(`Erro: ${error.message}`);
}
});
el('btn-fechar-usuarios').addEventListener('click', () => el('modal-usuarios').close());
el('form-novo-usuario').addEventListener('submit', async (event) => {
event.preventDefault();
try {
await API.post('/api/users', {
username: el('novo-usuario-username').value,
password: el('novo-usuario-password').value,
role: el('novo-usuario-role').value
});
el('novo-usuario-username').value = '';
el('novo-usuario-password').value = '';
await carregarListaUsuarios();
toast('Usuário criado');
} catch (error) {
toast(`Erro: ${error.message}`);
}
});
// abas
for (const tab of document.querySelectorAll('.tab')) {
tab.addEventListener('click', () => {
for (const t of document.querySelectorAll('.tab')) {
t.classList.toggle('active', t === tab);
t.setAttribute('aria-selected', t === tab ? 'true' : 'false');
}
for (const panel of document.querySelectorAll('.tab-panel')) {
panel.classList.toggle('active', panel.id === `tab-${tab.dataset.tab}`);
}
if (tab.dataset.tab === 'tratamento') Tratamento.ativar();
});
}
// configurações
let WABA_TEMPLATE_LIBRARY = [];
let COMMERCIAL_VARIABLES = [];
let focusedVariableInput = null;
let manualLeadContext = null;
async function carregarVariaveisComerciais() {
const { variables } = await API.get('/api/outreach/variables');
COMMERCIAL_VARIABLES = variables || [];
const picker = el('manual-variable-picker');
if (picker) {
picker.replaceChildren(
h('option', { value: '' }, 'Inserir variável da Máquina Comercial...'),
...COMMERCIAL_VARIABLES.map((variable) => h('option', { value: `{{${variable.key}}}` }, `{{${variable.key}}} - ${variable.label}`))
);
}
}
async function carregarBibliotecaWaba() {
const { templates, lastSync } = await API.get('/api/outreach/waba-templates');
WABA_TEMPLATE_LIBRARY = templates || [];
const select = el('manual-template-biblioteca');
if (select) select.replaceChildren(...templateOptions(select.value));
const statusEl = el('waba-template-library-status');
if (statusEl) {
statusEl.textContent = lastSync
? `${WABA_TEMPLATE_LIBRARY.length} templates carregados. Última atualização: ${dataBr(lastSync)}`
: `${WABA_TEMPLATE_LIBRARY.length} templates locais carregados. Para trazer todos do GChat, preencha o Token GChat e clique em Atualizar.`;
}
}
function templateLabel(template) {
return `${template.name} (${template.language})`;
}
function templateOptions(selected = '') {
return [h('option', { value: '' }, 'Selecione um template...')].concat(
WABA_TEMPLATE_LIBRARY.map((template) => h('option', {
value: template.name,
selected: template.name === selected ? '' : null
}, templateLabel(template)))
);
}
function preencherVariaveis(inputs, variables, fallback = []) {
inputs.forEach((input, index) => {
input.value = variables[index] || fallback[index] || '';
});
}
function templateVariableCount(template) {
if (!template) return 0;
const fromVariables = Array.isArray(template.variables) ? template.variables.length : 0;
const matches = String(template.text || '').match(/{{\s*\d+\s*}}/g) || [];
const fromText = Math.max(0, ...matches.map((match) => Number(match.replace(/\D/g, ''))));
return Math.min(5, Math.max(fromVariables, fromText));
}
function setVariableFields(container, template, inputs, fallback = []) {
const count = templateVariableCount(template);
inputs.forEach((input, index) => {
const field = input.closest('.template-var-field') || input.closest('label');
const visible = index < count;
if (field) field.classList.toggle('hidden', !visible);
if (!visible) input.value = '';
else if (!input.value) input.value = template?.variables?.[index] || fallback[index] || '';
});
const empty = container?.querySelector?.('.template-vars-empty');
if (empty) empty.classList.toggle('hidden', count > 0);
}
function previewTemplateText(template, inputs) {
const text = String(template?.text || '').trim();
if (!template) return 'Selecione um template para ver a pré-visualização.';
if (!text) return 'Pré-visualização indisponível para este template local.';
return text.replace(/{{\s*(\d+)\s*}}/g, (_, index) => {
const input = inputs[Number(index) - 1];
const value = resolverVariavelManual(input?.value?.trim());
return value ? `[${value}]` : `[Variável ${index}]`;
});
}
function atualizarPreview(previewEl, template, inputs) {
if (previewEl) previewEl.textContent = previewTemplateText(template, inputs);
}
function collectVisibleVariables(inputs) {
return inputs
.filter((input) => !(input.closest('.template-var-field') || input.closest('label'))?.classList.contains('hidden'))
.map((input) => input.value.trim());
}
function collectVisibleVariablesResolvidas(inputs) {
return collectVisibleVariables(inputs).map(resolverVariavelManual);
}
function valoresVariaveisManuais(lead = manualLeadContext) {
const empresa = lead?.empresa || lead?.legal_name || lead?.businessName || lead?.name || 'Empresa Teste';
const produto = lead?.produto || lead?.produto_principal || lead?.interesse || 'Atendimento comercial';
const segmento = lead?.segmento_label || lead?.segmento || 'Suporte';
const name = lead?.name || empresa;
return {
name,
empresa,
businessName: empresa,
firstName: String(name).split(/\s+/).filter(Boolean)[0] || empresa,
lastName: lead?.lastName || '',
produto,
protocol: lead?.protocol || produto,
cidade: lead?.cidade || 'Salvador',
uf: lead?.uf || 'BA',
segmento,
cnpj: lead?.cnpj || '00.000.000/0001-00',
email: lead?.email || 'cliente@exemplo.com.br',
phoneNumber: lead?.whatsapp || '5575999091780',
whatsapp: lead?.whatsapp || '5575999091780',
kanban: lead?.kanban || produto,
user: lead?.user || 'Grupo GTI',
userEmail: lead?.userEmail || 'comercial@gtigrupo.com.br',
greeting: 'Olá'
};
}
function resolverVariavelManual(value) {
const values = valoresVariaveisManuais();
let output = String(value || 'Informação')
.replaceAll('{empresa}', values.empresa)
.replaceAll('{produto}', values.produto)
.replaceAll('{cidade}', values.cidade)
.replaceAll('{segmento}', values.segmento);
for (const [key, variableValue] of Object.entries(values)) {
output = output.replaceAll(`{{${key}}}`, variableValue);
}
for (const variable of COMMERCIAL_VARIABLES) {
if (!output.includes(`{{${variable.key}}}`)) continue;
output = output.replaceAll(`{{${variable.key}}}`, values[variable.key] || variable.label);
}
return output;
}
const variavelExemplo = resolverVariavelManual;
function missingVisibleVariables(inputs) {
return inputs
.filter((input) => !(input.closest('.template-var-field') || input.closest('label'))?.classList.contains('hidden'))
.filter((input) => !input.value.trim());
}
function aplicarTemplateBiblioteca(template, { nomeEl, idiomaEl, routeEl, variableInputs }) {
if (!template) return;
nomeEl.value = template.name;
idiomaEl.value = template.language;
if (routeEl) routeEl.value = template.route;
preencherVariaveis(variableInputs, template.variables);
}
function variablePickerEl() {
return h('select', { class: 'variable-picker' },
h('option', { value: '' }, 'Inserir variável da Máquina Comercial...'),
...COMMERCIAL_VARIABLES.map((variable) => h('option', { value: `{{${variable.key}}}` }, `{{${variable.key}}} - ${variable.label}`))
);
}
async function carregarOutreach() {
const { settings } = await API.get('/api/settings');
el('outreach-ativo').checked = settings['outreach.ativo'] === 'true';
el('outreach-limite').value = settings['outreach.mensagensPorDia'] || 10;
const [{ templates }, status] = await Promise.all([
API.get('/api/outreach/templates'),
API.get('/api/outreach/status')
]);
el('outreach-status').textContent = `Enviado hoje: ${status.enviadosHoje}/${status.mensagensPorDia}`;
el('outreach-templates').replaceChildren(...templates.map((t) => {
const oficial = t.canal === 'oficial';
const mensagemEl = h('textarea', {
class: 'outreach-template-mensagem', rows: 2,
style: oficial ? 'display:none' : ''
}, t.mensagem);
const nomeEl = h('input', { type: 'text', class: 'outreach-template-waba-nome', value: t.template_waba_nome || '' });
const idiomaEl = h('input', { type: 'text', class: 'outreach-template-waba-idioma', value: t.template_waba_idioma || 'pt_BR' });
let variaveisSalvas = [];
try {
variaveisSalvas = JSON.parse(t.template_waba_variaveis_json || '[]');
} catch {
variaveisSalvas = [];
}
let templateSelecionado = WABA_TEMPLATE_LIBRARY.find((item) => item.name === t.template_waba_nome) || null;
const variaveisEls = Array.from({ length: 5 }, (_, index) => {
const input = h('input', {
type: 'text',
class: 'outreach-template-waba-var',
value: variaveisSalvas[index] || (index === 0 ? '{empresa}' : index === 1 ? '{produto}' : ''),
placeholder: index === 0 ? '{empresa}' : index === 1 ? '{produto}' : ''
});
input.addEventListener('focus', () => { focusedVariableInput = input; });
return h('label', { class: 'template-var-field' }, `Variável {{${index + 1}}}`, input);
});
const bibliotecaEl = h('select', { class: 'outreach-template-biblioteca' }, ...templateOptions(t.template_waba_nome || ''));
const variablePicker = variablePickerEl();
const previewEl = h('div', { class: 'template-preview' }, 'Selecione um template para ver a pré-visualização.');
variablePicker.addEventListener('change', () => {
if (!focusedVariableInput || !oficialEl.contains(focusedVariableInput) || !variablePicker.value) return;
focusedVariableInput.value = variablePicker.value;
focusedVariableInput.dispatchEvent(new Event('input'));
variablePicker.value = '';
});
bibliotecaEl.addEventListener('change', () => {
templateSelecionado = WABA_TEMPLATE_LIBRARY.find((item) => item.name === bibliotecaEl.value);
const inputs = Array.from(oficialEl.querySelectorAll('.outreach-template-waba-var'));
aplicarTemplateBiblioteca(templateSelecionado, {
nomeEl,
idiomaEl,
variableInputs: inputs
});
setVariableFields(oficialEl, templateSelecionado, inputs, ['{empresa}', '{produto}']);
atualizarPreview(previewEl, templateSelecionado, inputs);
atualizarAviso();
});
const avisoEl = h('p', { class: 'muted outreach-template-aviso hidden' },
'⚠️ Preencha o nome do template pra essa automação funcionar.');
const oficialEl = h('div', {
class: 'outreach-template-oficial',
style: oficial ? '' : 'display:none'
},
h('label', {}, 'Escolher modelo salvo', bibliotecaEl),
h('div', { class: 'row' },
h('label', {}, 'Nome do template aprovado', nomeEl),
h('label', {}, 'Idioma', idiomaEl)),
h('div', { class: 'template-vars outreach-template-vars' },
h('label', {}, 'Variáveis automáticas'),
h('p', { class: 'muted' }, 'Use placeholders como {empresa}, {produto}, {cidade}, {segmento}.'),
h('div', { class: 'variable-helper' },
variablePicker,
h('span', { class: 'muted' }, 'Clique em um campo abaixo e escolha uma variável.')),
h('p', { class: 'muted template-vars-empty hidden' }, 'Este template não exige variáveis.'),
h('div', { class: 'template-vars-grid' }, ...variaveisEls),
previewEl),
h('a', {
href: 'https://business.facebook.com/wa/manage/message-templates/',
target: '_blank', rel: 'noopener', class: 'outreach-template-link'
}, '↗ Ver/criar templates no Meta Business Manager'),
avisoEl);
const atualizarAviso = () => {
avisoEl.classList.toggle('hidden', radioOficial.checked ? Boolean(nomeEl.value.trim()) : true);
};
nomeEl.addEventListener('input', atualizarAviso);
for (const input of variaveisEls.map((label) => label.querySelector('input'))) {
input.addEventListener('input', () => atualizarPreview(previewEl, templateSelecionado, Array.from(oficialEl.querySelectorAll('.outreach-template-waba-var'))));
}
const radioNaoOficial = h('input', {
type: 'radio', name: `canal-${t.produto}`, class: 'outreach-template-canal', value: 'nao_oficial',
checked: oficial ? null : ''
});
const radioOficial = h('input', {
type: 'radio', name: `canal-${t.produto}`, class: 'outreach-template-canal', value: 'oficial',
checked: oficial ? '' : null
});
const alternarCanal = () => {
const usaOficial = radioOficial.checked;
mensagemEl.style.display = usaOficial ? 'none' : '';
oficialEl.style.display = usaOficial ? '' : 'none';
atualizarAviso();
};
radioNaoOficial.addEventListener('change', alternarCanal);
radioOficial.addEventListener('change', alternarCanal);
atualizarAviso();
queueMicrotask(() => {
const inputs = Array.from(oficialEl.querySelectorAll('.outreach-template-waba-var'));
setVariableFields(oficialEl, templateSelecionado, inputs, ['{empresa}', '{produto}']);
atualizarPreview(previewEl, templateSelecionado, inputs);
});
return h('div', { class: 'item outreach-template', 'data-produto': t.produto },
h('div', { class: 'row space-between' },
h('strong', {}, t.produto),
h('label', { class: 'radio' },
h('input', { type: 'checkbox', class: 'outreach-template-ativo', checked: t.ativo ? '' : null }),
' Ativo')),
h('div', { class: 'row' },
h('label', { class: 'radio' }, radioNaoOficial, ' Não-oficial (texto livre)'),
h('label', { class: 'radio' }, radioOficial, ' Oficial (Meta, template aprovado)')),
mensagemEl,
oficialEl);
}));
el('outreach-ultimos').replaceChildren(...status.ultimos.map((s) => {
const quem = s.origem === 'manual' ? (s.numero || 'número avulso') : `${s.empresa || '?'} (${s.cidade || '?'})`;
const rotulo = s.origem === 'manual' ? `Manual → ${quem}` : `${s.produto} → ${quem}`;
const statusIcone = s.status === 'enviado' ? '✅' : '❌';
return h('div', { class: 'item' },
h('div', {}, `${statusIcone} ${rotulo}`),
h('div', { class: 'sub' }, `${dataBr(s.sent_at)} · canal ${s.canal === 'oficial' ? 'oficial' : 'não-oficial'}${s.erro ? ` · ${s.erro}` : ''}`));
}));
if (!status.ultimos.length) {
el('outreach-ultimos').replaceChildren(h('p', { class: 'muted' }, 'Nenhum envio ainda.'));
}
}
el('btn-settings').addEventListener('click', async () => {
const { settings } = await API.get('/api/settings');
el('cfg-pipeline').value = settings['zpro.pipelineId'] || 1;
el('cfg-stage').value = settings['zpro.stageId'] || 1;
el('cfg-responsible').value = settings['zpro.responsibleId'] || 15;
el('cfg-ticket-queue').value = settings['zpro.ticketQueueId'] || 12;
el('cfg-forecast').value = settings['zpro.closingForecastDays'] || 30;
el('cfg-zpro-baseurl').value = settings['zpro.baseUrl'] || '';
el('cfg-zpro-apiid').value = settings['zpro.apiId'] || '';
el('cfg-zpro-token').value = settings['zpro.bearerToken'] || '';
el('cfg-zpro-whatsappid').value = settings['zpro.whatsappId'] || '';
el('cfg-gchat-templates-url').value = settings['gchat.templatesUrl'] || '';
el('cfg-gchat-token').value = settings['gchat.bearerToken'] || '';
el('zpro-credenciais-status').textContent = '';
el('modal-settings').showModal();
try {
await carregarBibliotecaWaba();
await carregarVariaveisComerciais();
await carregarOutreach();
} catch (error) {
toast(`Erro ao carregar automação: ${error.message}`);
}
});
el('btn-testar-zpro').addEventListener('click', async () => {
const statusEl = el('zpro-credenciais-status');
statusEl.textContent = 'Salvando e testando...';
try {
await API.put('/api/settings', {
'zpro.baseUrl': el('cfg-zpro-baseurl').value.trim(),
'zpro.apiId': el('cfg-zpro-apiid').value.trim(),
'zpro.bearerToken': el('cfg-zpro-token').value.trim(),
'zpro.whatsappId': el('cfg-zpro-whatsappid').value.trim(),
'gchat.templatesUrl': el('cfg-gchat-templates-url').value.trim(),
'gchat.bearerToken': el('cfg-gchat-token').value.trim(),
'outreach.ativo': el('outreach-ativo').checked ? 'true' : 'false',
'outreach.mensagensPorDia': el('outreach-limite').value
});
const resultado = await API.post('/api/outreach/testar-conexao');
const diag = resultado.diagnostico
? `\nBase: ${resultado.diagnostico.baseUrl || '-'} | API ID: ${resultado.diagnostico.apiId || '-'} | Token: ${resultado.diagnostico.token || '-'} | WhatsApp ID: ${resultado.diagnostico.whatsappId || '-'}`
: '';
statusEl.textContent = resultado.ok ? `✅ Conexão OK${diag}` : `❌ ${resultado.erro}${diag}`;
} catch (error) {
statusEl.textContent = `❌ ${error.message}`;
}
});
el('btn-sync-gchat-templates').addEventListener('click', async () => {
await sincronizarTemplatesGchat();
});
el('btn-sync-gchat-templates-inline').addEventListener('click', async () => {
await sincronizarTemplatesGchat();
});
async function sincronizarTemplatesGchat() {
const statusEl = el('gchat-template-sync-status');
const inlineStatusEl = el('waba-template-library-status');
statusEl.textContent = 'Sincronizando...';
if (inlineStatusEl) inlineStatusEl.textContent = 'Sincronizando com GChat...';
try {
await API.put('/api/settings', {
'gchat.templatesUrl': el('cfg-gchat-templates-url').value.trim(),
'gchat.bearerToken': el('cfg-gchat-token').value.trim()
});
const result = await API.post('/api/outreach/waba-templates/sync');
statusEl.textContent = result.ok ? `✅ ${result.total} templates sincronizados` : `❌ ${result.erro}`;
if (inlineStatusEl) inlineStatusEl.textContent = result.ok
? `✅ ${result.total} templates sincronizados do GChat`
: `❌ ${result.erro}. Preencha o Token GChat opcional em Credenciais Z-PRO.`;
await carregarBibliotecaWaba();
await carregarOutreach();
} catch (error) {
statusEl.textContent = `❌ ${error.message}`;
if (inlineStatusEl) inlineStatusEl.textContent = `❌ ${error.message}`;
}
}
el('btn-salvar-outreach').addEventListener('click', async () => {
try {
await API.put('/api/settings', {
'outreach.ativo': el('outreach-ativo').checked ? 'true' : 'false',
'outreach.mensagensPorDia': el('outreach-limite').value
});
for (const item of el('outreach-templates').children) {
const canalEl = item.querySelector('.outreach-template-canal:checked');
await API.put(`/api/outreach/templates/${encodeURIComponent(item.dataset.produto)}`, {
mensagem: item.querySelector('.outreach-template-mensagem').value,
ativo: item.querySelector('.outreach-template-ativo').checked,
canal: canalEl ? canalEl.value : 'nao_oficial',
templateWabaNome: item.querySelector('.outreach-template-waba-nome').value,
templateWabaIdioma: item.querySelector('.outreach-template-waba-idioma').value,
templateWabaVariaveis: collectVisibleVariables(Array.from(item.querySelectorAll('.outreach-template-waba-var')))
});
}
await carregarOutreach();
toast('Automação salva');
} catch (error) {
toast(`Erro: ${error.message}`);
}
});
function alternarCanalManual() {
const oficial = el('manual-canal-oficial').checked;
el('manual-mensagem-wrap').classList.toggle('hidden', oficial);
el('manual-oficial-campos').classList.toggle('hidden', !oficial);
}
el('manual-canal-naooficial').addEventListener('change', alternarCanalManual);
el('manual-canal-oficial').addEventListener('change', alternarCanalManual);
el('manual-template-biblioteca').replaceChildren(...templateOptions());
el('manual-template-biblioteca').addEventListener('change', () => {
const template = WABA_TEMPLATE_LIBRARY.find((item) => item.name === el('manual-template-biblioteca').value);
const inputs = Array.from(document.querySelectorAll('.manual-template-var'));
aplicarTemplateBiblioteca(template, {
nomeEl: el('manual-template-nome'),
idiomaEl: el('manual-template-idioma'),
routeEl: el('manual-template-rota'),
variableInputs: inputs
});
setVariableFields(el('manual-oficial-campos'), template, inputs);
atualizarPreview(el('manual-template-preview'), template, inputs);
});
for (const input of document.querySelectorAll('.manual-template-var')) {
input.addEventListener('focus', () => { focusedVariableInput = input; });
input.addEventListener('input', () => {
const template = WABA_TEMPLATE_LIBRARY.find((item) => item.name === el('manual-template-biblioteca').value);
atualizarPreview(el('manual-template-preview'), template, Array.from(document.querySelectorAll('.manual-template-var')));
});
}
el('manual-variable-picker').addEventListener('change', () => {
if (!focusedVariableInput || !el('manual-variable-picker').value) return;
focusedVariableInput.value = el('manual-variable-picker').value;
focusedVariableInput.dispatchEvent(new Event('input'));
el('manual-variable-picker').value = '';
});
setVariableFields(el('manual-oficial-campos'), null, Array.from(document.querySelectorAll('.manual-template-var')));
el('btn-testar-template-biblioteca').addEventListener('click', async () => {
const statusEl = el('manual-status');
const template = WABA_TEMPLATE_LIBRARY.find((item) => item.name === el('manual-template-biblioteca').value);
const numero = el('manual-numero').value.trim();
statusEl.classList.remove('status-ok', 'status-error');
if (!template) {
statusEl.textContent = '❌ Selecione um modelo salvo primeiro.';
statusEl.classList.add('status-error');
return;
}
if (!numero) {
statusEl.textContent = '❌ Informe um número de teste acima.';
statusEl.classList.add('status-error');
return;
}
aplicarTemplateBiblioteca(template, {
nomeEl: el('manual-template-nome'),
idiomaEl: el('manual-template-idioma'),
routeEl: el('manual-template-rota'),
variableInputs: Array.from(document.querySelectorAll('.manual-template-var'))
});
const variaveis = template.variables.map(resolverVariavelManual);
preencherVariaveis(Array.from(document.querySelectorAll('.manual-template-var')), variaveis);
setVariableFields(el('manual-oficial-campos'), template, Array.from(document.querySelectorAll('.manual-template-var')));
atualizarPreview(el('manual-template-preview'), template, Array.from(document.querySelectorAll('.manual-template-var')));
statusEl.textContent = `Testando ${template.name}...`;
try {
const resultado = await API.post('/api/outreach/enviar-manual', {
numero,
canal: 'oficial',
mensagem: '',
templateWabaNome: template.name,
templateWabaIdioma: template.language,
templateRota: template.route,
templateVariaveis: variaveis
});
statusEl.textContent = resultado.ok
? `✅ ${template.name} funcionou${resultado.ticketId ? ` — ticket ${resultado.ticketId}` : ''}`
: `❌ ${template.name} falhou: ${resultado.erro}`;
statusEl.classList.toggle('status-ok', Boolean(resultado.ok));
statusEl.classList.toggle('status-error', !resultado.ok);
await carregarOutreach();
} catch (error) {
statusEl.textContent = `❌ ${template.name} falhou: ${error.message}`;
statusEl.classList.add('status-error');
}
});
el('btn-toggle-outreach-log').addEventListener('click', () => {
const log = el('outreach-ultimos');
const hidden = log.classList.toggle('hidden');
el('btn-toggle-outreach-log').textContent = hidden ? 'Ver log de envios' : 'Ocultar log de envios';
});
el('btn-enviar-manual').addEventListener('click', async () => {
const statusEl = el('manual-status');
const oficial = el('manual-canal-oficial').checked;
statusEl.classList.remove('status-ok', 'status-error');
if (oficial) {
const faltando = missingVisibleVariables(Array.from(document.querySelectorAll('.manual-template-var')));
if (faltando.length) {
statusEl.textContent = `❌ Preencha todas as variáveis do template (${faltando.length} pendente${faltando.length > 1 ? 's' : ''}).`;
statusEl.classList.add('status-error');
return;
}
}
statusEl.textContent = 'Enviando...';
try {
const resultado = await API.post('/api/outreach/enviar-manual', {
numero: el('manual-numero').value.trim(),
canal: oficial ? 'oficial' : 'nao_oficial',
mensagem: el('manual-mensagem').value,
templateWabaNome: el('manual-template-nome').value.trim(),
templateWabaIdioma: el('manual-template-idioma').value.trim(),
templateVariaveis: collectVisibleVariablesResolvidas(Array.from(document.querySelectorAll('.manual-template-var'))),
templateRota: el('manual-template-rota').value,
lead: manualLeadContext
});
statusEl.textContent = resultado.ok
? `✅ Enviado${resultado.ticketId ? ` — ticket ${resultado.ticketId}` : ''}`
: `❌ ${resultado.erro}`;
statusEl.classList.toggle('status-ok', Boolean(resultado.ok));
statusEl.classList.toggle('status-error', !resultado.ok);
await carregarOutreach();
} catch (error) {
statusEl.textContent = `❌ ${error.message}`;
statusEl.classList.add('status-error');
}
});
window.GTISelecionarLeadManual = (lead) => {
manualLeadContext = lead || null;
const template = WABA_TEMPLATE_LIBRARY.find((item) => item.name === el('manual-template-biblioteca').value);
atualizarPreview(el('manual-template-preview'), template, Array.from(document.querySelectorAll('.manual-template-var')));
};
el('btn-salvar-settings').addEventListener('click', async (event) => {
event.preventDefault();
try {
await API.put('/api/settings', {
'zpro.pipelineId': el('cfg-pipeline').value,
'zpro.stageId': el('cfg-stage').value,
'zpro.responsibleId': el('cfg-responsible').value,
'zpro.ticketQueueId': el('cfg-ticket-queue').value,
'zpro.closingForecastDays': el('cfg-forecast').value,
'zpro.baseUrl': el('cfg-zpro-baseurl').value.trim(),
'zpro.apiId': el('cfg-zpro-apiid').value.trim(),
'zpro.bearerToken': el('cfg-zpro-token').value.trim(),
'zpro.whatsappId': el('cfg-zpro-whatsappid').value.trim(),
'gchat.templatesUrl': el('cfg-gchat-templates-url').value.trim(),
'gchat.bearerToken': el('cfg-gchat-token').value.trim(),
'outreach.ativo': el('outreach-ativo').checked ? 'true' : 'false',
'outreach.mensagensPorDia': el('outreach-limite').value
});
await carregarOutreach();
el('modal-settings').close();
toast('Configurações salvas');
} catch (error) {
toast(`Erro: ${error.message}`);
}
});
// limpar toda a base (zona de risco)
el('btn-limpar-base').addEventListener('click', () => {
el('reset-base-senha').value = '';
el('reset-base-erro').classList.add('hidden');
el('modal-settings').close();
el('modal-reset-base').showModal();
});
el('btn-cancelar-reset-base').addEventListener('click', () => {
el('modal-reset-base').close();
});
// Erro aqui precisa aparecer DENTRO do modal (nao so no toast): um <dialog>
// aberto via showModal() fica na "top layer" do navegador, acima de
// qualquer div normal — inclusive do toast, que ficava invisivel atras do
// modal quando a senha estava errada, dando a impressao de que nada
// acontecia (bug reportado: "limpeza nao foi feita mesmo pondo a senha").
el('btn-confirmar-reset-base').addEventListener('click', async () => {
const erroEl = el('reset-base-erro');
const senha = el('reset-base-senha').value;
if (!senha) {
erroEl.textContent = 'Informe a senha de segurança';
erroEl.classList.remove('hidden');
return;
}
try {
await API.post('/api/admin/reset', { senha });
el('modal-reset-base').close();
toast('Base de leads limpa com sucesso — recarregando...');
setTimeout(() => window.location.reload(), 1200);
} catch (error) {
erroEl.textContent = `Erro: ${error.message}`;
erroEl.classList.remove('hidden');
}
});
// fecha modais clicando fora
for (const dialog of document.querySelectorAll('dialog')) {
dialog.addEventListener('click', (event) => {
if (event.target === dialog) dialog.close();
});
}
Tratamento.bindEvents();
try {
await Captura.init();
} catch (error) {
toast(`Erro ao carregar: ${error.message}`, 8000);
}
})();