Per-product message templates, official/não-oficial channel selection, manual per-number sending, and a 20-minute internal scheduler for the existing automatic outreach batch — all pre-existing local work being committed as-is.
685 lines
30 KiB
JavaScript
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);
|
|
}
|
|
})();
|