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:
+96
-16
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user