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.
This commit is contained in:
+550
-3
@@ -68,15 +68,543 @@
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -84,8 +612,18 @@
|
||||
'zpro.pipelineId': el('cfg-pipeline').value,
|
||||
'zpro.stageId': el('cfg-stage').value,
|
||||
'zpro.responsibleId': el('cfg-responsible').value,
|
||||
'zpro.closingForecastDays': el('cfg-forecast').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) {
|
||||
@@ -96,6 +634,7 @@
|
||||
// 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();
|
||||
});
|
||||
@@ -104,10 +643,17 @@
|
||||
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) {
|
||||
toast('Informe a senha de segurança');
|
||||
erroEl.textContent = 'Informe a senha de segurança';
|
||||
erroEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -116,7 +662,8 @@
|
||||
toast('Base de leads limpa com sucesso — recarregando...');
|
||||
setTimeout(() => window.location.reload(), 1200);
|
||||
} catch (error) {
|
||||
toast(`Erro: ${error.message}`);
|
||||
erroEl.textContent = `Erro: ${error.message}`;
|
||||
erroEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user