Foto automática, boleto na conversa e novo Dashboard

- Foto do cliente atualizada automaticamente ao abrir a conversa (remove botão manual)
- Exibe CLI_FOTO na tela de Dados do Cliente; matrícula com conversão robusta
- Envio de boleto na conversa (checkbox em Configurações > Empresa): lista títulos em aberto e envia texto com linha digitável + PIX ao cliente
- Novo Dashboard (gerente): conversas (abertas/não atendidas/não atribuídas/pendentes), atendentes disponíveis/desconectados e heatmap de tráfego
- Presença de atendentes em memória + ping periódico do chat/dashboard

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 15:41:09 -03:00
parent aa74c984d0
commit d9e195d924
13 changed files with 657 additions and 128 deletions
+96 -16
View File
@@ -651,6 +651,18 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
</div>
</div>
<!-- Modal enviar boleto -->
<div class="modal-overlay" id="modalBoleto">
<div class="modal">
<h3>💳 Enviar boleto</h3>
<p style="font-size:13px;color:#6b7280;margin-bottom:12px">Selecione o título a enviar para o cliente:</p>
<div id="boletoLista" style="max-height:320px;overflow-y:auto"></div>
<div class="btn-group">
<button onclick="fecharModalBoleto()">Fechar</button>
</div>
</div>
</div>
<div class="sidebar-left">
<div class="header">
<h2>💬 Chatc2</h2>
@@ -917,7 +929,7 @@ function renderConversas(convs, busca) {
}
// ===== ABRIR CONVERSA =====
window.abrirConversa = async function(id) {
window.abrirConversa = async function(id, skipFotoRefresh) {
conversaAtiva = id;
// Mobile: alterna para a visão da conversa (e fecha o painel de info)
document.body.classList.add('chat-aberto');
@@ -941,11 +953,26 @@ window.abrirConversa = async function(id) {
var data = await res.json();
if (!data.success) return;
renderInfoConversa(data.data);
// Foto automática: se o cliente está vinculado mas sem foto, busca via Evolution
// em segundo plano e recarrega o painel quando a foto chegar.
if (!skipFotoRefresh && data.data && data.data.cliente && !data.data.cliente.foto) {
autoAtualizarFoto(id);
}
} catch(e) {}
carregarMensagens();
};
// Busca a foto do cliente via Evolution (fetchProfile) em segundo plano, sem bloquear.
function autoAtualizarFoto(id) {
fetch('/api/' + alias + '/evolution/refresh-photo/' + id, {
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }
}).then(function(r){ return r.json(); }).then(function(r){
// Recarrega apenas se a foto foi atualizada e ainda estamos na mesma conversa
if (r && r.success && r.atualizada && conversaAtiva === id) abrirConversa(id, true);
}).catch(function(){});
}
function renderInfoConversa(conv) {
document.getElementById('chatNome').textContent = conv.nomeContato || conv.numero || 'Desconhecido';
document.getElementById('chatStatus').textContent = conv.status === 'E' ? '🟡 Em espera' : conv.status === 'A' ? '🟢 Em atendimento' : '⚫ Finalizado';
@@ -986,8 +1013,7 @@ function renderInfoConversa(conv) {
(c.matricula ? '<div class="field"><div class="label">Matrícula</div><div class="value">' + esc(c.matricula) + '</div></div>' : '') +
(c.plano ? '<div class="field"><div class="label">Plano</div><div class="value">' + esc(c.plano) + '</div></div>' : '') +
'<div class="field"><div class="label">Contato</div><div class="value">' + esc(c.celular || c.telefone || conv.numero || '-') + '</div></div>' +
'<div class="field"><div class="label ' + inadClass + '">' + inadLabel + '</div></div>' +
'<button id="btnAtualizarFoto" onclick="atualizarFotoCliente()" style="margin-top:8px;padding:6px 10px;border:1px solid #e5e7eb;border-radius:6px;background:#f9fafb;cursor:pointer;font-size:12px;color:#374151">🔄 Atualizar foto (WhatsApp)</button>';
'<div class="field"><div class="label ' + inadClass + '">' + inadLabel + '</div></div>';
} else if (conv.dependente) {
var dep = conv.dependente;
fotoContainer.textContent = (dep.nome || '?').charAt(0).toUpperCase();
@@ -1005,6 +1031,13 @@ function renderInfoConversa(conv) {
infoContainer.innerHTML = '<div style="color:#9ca3af;font-size:13px;text-align:center;padding:20px 0">Contato não cadastrado como cliente</div>';
}
// Botão "Enviar boleto" (aparece se a empresa habilitar e houver titular)
var boletoClienteId = conv.cliente ? conv.cliente.id : (conv.dependente ? conv.dependente.titularId : null);
if (cfgResolucao.enviarBoleto === 'S' && boletoClienteId && conv.status !== 'F') {
infoContainer.innerHTML +=
'<button id="btnEnviarBoleto" onclick="abrirModalBoleto(' + boletoClienteId + ')" style="margin-top:10px;width:100%;padding:8px 10px;border:1px solid #c7d2fe;border-radius:6px;background:#eef2ff;cursor:pointer;font-size:13px;color:#4338ca;font-weight:600">💳 Enviar boleto</button>';
}
// Verifica convalescentes pendentes (situação E = entrega)
var clienteId = conv.cliente ? conv.cliente.id : (conv.dependente ? conv.dependente.titularId : null);
if (clienteId) {
@@ -1488,24 +1521,62 @@ window.toggleInfo = function() {
document.body.classList.toggle('info-aberto');
};
// Busca a foto do cliente via Evolution (fetchProfile) e atualiza o painel
window.atualizarFotoCliente = async function() {
if (!conversaAtiva) return;
var btn = document.getElementById('btnAtualizarFoto');
if (btn) { btn.disabled = true; btn.textContent = '⏳ Buscando...'; }
// ===== ENVIAR BOLETO =====
var boletoClienteAtual = null;
window.abrirModalBoleto = async function(clienteId) {
if (!conversaAtiva || !clienteId) return;
boletoClienteAtual = clienteId;
var modal = document.getElementById('modalBoleto');
var lista = document.getElementById('boletoLista');
modal.classList.add('show');
lista.innerHTML = '<div style="text-align:center;color:#9ca3af;padding:20px">⏳ Carregando títulos...</div>';
try {
var r = await (await fetch('/api/' + alias + '/evolution/refresh-photo/' + conversaAtiva, {
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }
var r = await (await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/boletos?clienteId=' + clienteId, {
headers: { 'Authorization': 'Bearer ' + token }
})).json();
if (r.success && r.atualizada) {
abrirConversa(conversaAtiva); // recarrega para exibir a nova foto
if (!r.success) { lista.innerHTML = '<div style="color:#ef4444;padding:16px">' + esc(r.error || 'Erro ao carregar títulos.') + '</div>'; return; }
var carnes = (r.data || []).filter(function(c){ return c.temLinhaDigitavel || c.temPix; });
if (carnes.length === 0) {
lista.innerHTML = '<div style="color:#9ca3af;padding:16px;text-align:center">Nenhum título em aberto com boleto/PIX disponível.</div>';
return;
}
lista.innerHTML = carnes.map(function(c){
var valor = (c.valor != null) ? ('R$ ' + Number(c.valor).toFixed(2).replace('.', ',')) : '-';
var venc = c.vencimento ? c.vencimento.split('-').reverse().join('/') : '-';
var parc = c.parcela ? (' • Parcela ' + c.parcela + (c.totalParcelas ? '/' + c.totalParcelas : '')) : '';
var tags = (c.temPix ? '<span style="font-size:10px;background:#dcfce7;color:#166534;padding:1px 6px;border-radius:8px;margin-left:6px">PIX</span>' : '') +
(c.temLinhaDigitavel ? '<span style="font-size:10px;background:#dbeafe;color:#1e40af;padding:1px 6px;border-radius:8px;margin-left:6px">Boleto</span>' : '');
return '<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;padding:10px;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:8px">' +
'<div><div style="font-weight:600;color:#111827">' + valor + tags + '</div>' +
'<div style="font-size:12px;color:#6b7280">Vencimento: ' + venc + parc + '</div></div>' +
'<button onclick="enviarBoleto(' + c.id + ', this)" style="padding:6px 12px;background:#4f46e5;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;white-space:nowrap">Enviar</button>' +
'</div>';
}).join('');
} catch(e) {
lista.innerHTML = '<div style="color:#ef4444;padding:16px">Erro de conexão.</div>';
}
};
window.fecharModalBoleto = function() {
document.getElementById('modalBoleto').classList.remove('show');
};
window.enviarBoleto = async function(carneId, btn) {
if (!conversaAtiva || !boletoClienteAtual) return;
if (btn) { btn.disabled = true; btn.textContent = 'Enviando...'; }
try {
var r = await (await fetch('/api/' + alias + '/conversations/' + conversaAtiva + '/send-boleto', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ clienteId: boletoClienteAtual, carneId: carneId })
})).json();
if (r.success) {
fecharModalBoleto();
carregarMensagens();
} else if (btn) {
btn.disabled = false;
btn.textContent = r.message || 'Nenhuma foto encontrada';
setTimeout(function(){ btn.textContent = '🔄 Atualizar foto (WhatsApp)'; }, 2500);
btn.disabled = false; btn.textContent = 'Enviar';
alert(r.error || 'Não foi possível enviar o boleto.');
}
} catch(e) {
if (btn) { btn.disabled = false; btn.textContent = '🔄 Atualizar foto (WhatsApp)'; }
if (btn) { btn.disabled = false; btn.textContent = 'Enviar'; }
}
};
@@ -1665,6 +1736,15 @@ setInterval(function() {
if (conversaAtiva) carregarMensagens();
}, 5000);
// Presença: marca o atendente como "online" para o dashboard
function pingPresenca() {
fetch('/api/' + alias + '/dashboard/ping', {
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }
}).catch(function(){});
}
pingPresenca();
setInterval(pingPresenca, 45000);
// Auto-resize textarea
document.getElementById('msgInput').addEventListener('input', function() {
this.style.height = 'auto';