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';
+8 -1
View File
@@ -209,9 +209,16 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
var sitLabel = c.situacao ? c.situacao.descricao : 'Desconhecido';
var iniciais = c.nome ? c.nome.split(' ').map(function(s) { return s[0]; }).slice(0,2).join('').toUpperCase() : '?';
var avatarConteudo = iniciais;
if (c.foto) {
var srcFoto = c.foto;
if (srcFoto.indexOf('data:') !== 0 && srcFoto.length > 50) srcFoto = 'data:image/jpeg;base64,' + srcFoto;
avatarConteudo = '<img src="' + srcFoto + '" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%">';
}
container.innerHTML =
'<div class="client-header">' +
'<div class="client-avatar">' + iniciais + '</div>' +
'<div class="client-avatar">' + avatarConteudo + '</div>' +
'<div class="client-header-info">' +
'<h1>' + (c.nome || '-') + '</h1>' +
'<div class="matricula">Matrícula: ' + (c.matricula || '-') + '</div>' +
+178 -103
View File
@@ -9,6 +9,35 @@
body { background: #f3f4f6; display: flex; min-height: 100vh; }
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.container { flex: 1; padding: 24px; overflow-y: auto; }
.section-title { font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: #6b7280; margin: 4px 0 12px; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 14px; margin-bottom: 24px; }
.stat-card { background: #fff; border-radius: 12px; padding: 18px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); border-left: 4px solid #9ca3af; }
.stat-card .num { font-size: 32px; font-weight: 800; color: #111827; line-height: 1; }
.stat-card .lbl { font-size: 13px; color: #6b7280; margin-top: 6px; font-weight: 600; }
.stat-card .desc { font-size: 11px; color: #9ca3af; margin-top: 4px; }
.stat-card.abertas { border-left-color: #10b981; }
.stat-card.naoatendidas { border-left-color: #ef4444; }
.stat-card.naoatribuidas { border-left-color: #f59e0b; }
.stat-card.pendentes { border-left-color: #6366f1; }
.stat-card.disponiveis { border-left-color: #10b981; }
.stat-card.desconectados { border-left-color: #9ca3af; }
.card { background:#fff; border-radius:12px; padding:18px; box-shadow:0 1px 3px rgba(0,0,0,0.08); margin-bottom:24px; }
.atendente-row { display:flex; align-items:center; gap:8px; padding:7px 0; border-bottom:1px solid #f3f4f6; font-size:14px; }
.atendente-row:last-child { border-bottom:none; }
.dot { width:9px; height:9px; border-radius:50%; flex-shrink:0; }
.dot.on { background:#10b981; box-shadow:0 0 0 3px rgba(16,185,129,.18); }
.dot.off { background:#cbd5e1; }
.atendente-row .st { margin-left:auto; font-size:12px; color:#6b7280; }
/* Heatmap (estilo GitHub) */
.heatmap-scroll { overflow-x:auto; padding-bottom:6px; }
.heatmap { display:flex; gap:3px; }
.hm-col { display:flex; flex-direction:column; gap:3px; }
.hm-cell { width:12px; height:12px; border-radius:2px; background:#ebedf0; }
.hm-cell.l1 { background:#9be9a8; } .hm-cell.l2 { background:#40c463; }
.hm-cell.l3 { background:#30a14e; } .hm-cell.l4 { background:#216e39; }
.hm-months { display:flex; gap:3px; font-size:10px; color:#9ca3af; margin-bottom:4px; height:12px; }
.hm-legend { display:flex; align-items:center; gap:4px; font-size:11px; color:#9ca3af; margin-top:8px; justify-content:flex-end; }
body.dark-mode .hm-cell { background:#161b22; }
</style>
<link rel="stylesheet" href="/css/dark-mode.css">
<script>function darkModeToggle(){var e=document.body;if(!e)return;var a=localStorage.getItem('chatc2_dark_mode')!=='true';e.classList.toggle('dark-mode',a);localStorage.setItem('chatc2_dark_mode',a?'true':'false');document.querySelectorAll('.dark-mode-toggle').forEach(function(b){b.innerHTML=a?' Claro':'🌙 Escuro'});}
@@ -27,29 +56,17 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
</div>
<nav class="sidebar-nav">
<div class="nav-label">Principal</div>
<a href="#" class="active" id="navDashboard">
<span class="icon">📊</span> Dashboard
</a>
<a href="#" id="navClients">
<span class="icon">👥</span> Clientes
</a>
<a href="#" id="navChat">
<span class="icon">💬</span> Conversas
</a>
<a href="#" class="active" id="navDashboard"><span class="icon">📊</span> Dashboard</a>
<a href="#" id="navClients"><span class="icon">👥</span> Clientes</a>
<a href="#" id="navChat"><span class="icon">💬</span> Conversas</a>
<div class="nav-label" id="adminLabel" style="display:none">Administrador</div>
<a href="#" id="navConfig" style="display:none">
<span class="icon">⚙️</span> Configurações
</a>
<a href="#" id="navConfig" style="display:none"><span class="icon">⚙️</span> Configurações</a>
<a href="#" id="navAllConvs" style="display:none"><span class="icon">💬</span> Todas Conversas</a>
<a href="#" id="navRoutes" style="display:none">
<span class="icon">📡</span> Rotas
</a>
<a href="#" id="navRoutes" style="display:none"><span class="icon">📡</span> Rotas</a>
</nav>
<div class="sidebar-footer">
<button class="dark-mode-toggle" onclick="darkModeToggle()" style="width:100%;margin-bottom:8px;padding:8px">🌙 Escuro</button>
<a onclick="logout()">
<span class="icon">🚪</span> Sair
</a>
<a onclick="logout()"><span class="icon">🚪</span> Sair</a>
</div>
</aside>
@@ -64,123 +81,181 @@ window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')
</div>
<div class="container">
<div class="card">
<h3>👤 Dados do Usuário</h3>
<div class="info-grid">
<div class="info-item">
<div class="label">ID</div>
<div class="value" id="userId">-</div>
</div>
<div class="info-item">
<div class="label">Nome</div>
<div class="value" id="userNameDisplay">-</div>
</div>
<div class="info-item">
<div class="label">Login</div>
<div class="value" id="userLogin">-</div>
</div>
<div class="info-item">
<div class="label">Email</div>
<div class="value" id="userEmail">-</div>
</div>
<div class="info-item">
<div class="label">Nível</div>
<div class="value" id="userNivel">-</div>
</div>
<div class="info-item">
<div class="label">Tipo</div>
<div class="value" id="userTipo">-</div>
</div>
<div class="info-item">
<div class="label">Autenticação</div>
<div class="value" id="userAuthType">-</div>
</div>
</div>
<div class="section-title">Conversas</div>
<div class="stat-grid">
<div class="stat-card abertas"><div class="num" id="cAbertas"></div><div class="lbl">Abertas</div><div class="desc">Com atendente e equipe</div></div>
<div class="stat-card naoatendidas"><div class="num" id="cNaoAtendidas"></div><div class="lbl">Não Atendidas</div><div class="desc">Aguardando resposta do atendente</div></div>
<div class="stat-card naoatribuidas"><div class="num" id="cNaoAtribuidas"></div><div class="lbl">Não Atribuídas</div><div class="desc">Sem equipe e sem atendente</div></div>
<div class="stat-card pendentes"><div class="num" id="cPendentes"></div><div class="lbl">Pendentes</div><div class="desc">Somente com equipe</div></div>
</div>
<div class="section-title">Atendentes</div>
<div class="stat-grid">
<div class="stat-card disponiveis"><div class="num" id="aDisponiveis"></div><div class="lbl">Disponíveis</div><div class="desc">Acessando a plataforma</div></div>
<div class="stat-card desconectados"><div class="num" id="aDesconectados"></div><div class="lbl">Desconectados</div><div class="desc">Fora da plataforma</div></div>
</div>
<div class="card">
<h3>🔑 Token de Acesso</h3>
<button class="btn-copy" onclick="showToken()" id="btnShowToken" style="margin-bottom:8px">Mostrar Token</button>
<div class="token-box" id="tokenDisplay" style="display:none"></div>
<button class="btn-copy" onclick="copyToken()" id="btnCopyToken" style="display:none">Copiar Token</button>
<div class="section-title" style="margin-top:0">Situação dos atendentes</div>
<div id="atendentesLista"><div style="color:#9ca3af;font-size:13px">Carregando...</div></div>
</div>
<div class="section-title">Tráfego das Conversas</div>
<div class="card">
<div style="font-size:13px;color:#6b7280;margin-bottom:12px" id="trafegoResumo">Conversas iniciadas nos últimos 12 meses</div>
<div class="heatmap-scroll">
<div class="hm-months" id="hmMonths"></div>
<div class="heatmap" id="heatmap"></div>
</div>
<div class="hm-legend">
Menos
<span class="hm-cell"></span><span class="hm-cell l1"></span><span class="hm-cell l2"></span><span class="hm-cell l3"></span><span class="hm-cell l4"></span>
Mais
</div>
</div>
</div>
</div>
<script>
// Extrai alias da URL
const pathParts = window.location.pathname.split('/');
const alias = pathParts[2] || localStorage.getItem('chatc2_alias') || 'lajedo';
var pathParts = window.location.pathname.split('/');
var alias = pathParts[2] || localStorage.getItem('chatc2_alias') || 'lajedo';
localStorage.setItem('chatc2_alias', alias);
const token = localStorage.getItem('chatc2_token');
const user = JSON.parse(localStorage.getItem('chatc2_user') || '{}');
var token = localStorage.getItem('chatc2_token');
var user = JSON.parse(localStorage.getItem('chatc2_user') || '{}');
if (!token) {
window.location.href = '/app/' + alias + '/login';
}
if (!token) { window.location.href = '/app/' + alias + '/login'; }
// Sidebar - mostra opções de admin se for Gerente
const tipoChat = user.tipoChat || 'A';
var tipoChat = user.tipoChat || 'A';
if (tipoChat === 'G') {
document.getElementById('adminLabel').style.display = '';
document.getElementById('navConfig').style.display = '';
document.getElementById('navAllConvs').style.display = '';
document.getElementById('navRoutes').style.display = '';
} else {
// Agente não dashboard - redireciona para conversas
window.location.href = '/app/' + alias + '/company/' + (user.empresas?.[0] || 1) + '/conversation/0';
// Agente não acessa o dashboard — vai direto para as conversas
window.location.href = '/app/' + alias + '/company/' + (user.empresas && user.empresas[0] || 1) + '/conversation/0';
}
document.getElementById('sidebarAlias').textContent = alias;
document.getElementById('userId').textContent = user.id || '-';
document.getElementById('userNameDisplay').textContent = user.nome || '-';
document.getElementById('userName').textContent = user.nome || '-';
document.getElementById('userLogin').textContent = user.login || '-';
document.getElementById('userEmail').textContent = user.email || '-';
document.getElementById('userNivel').textContent = user.nivelId || '-';
document.getElementById('userTipo').textContent = user.tipo || '-';
document.getElementById('userAuthType').textContent = user.authType || 'jwt';
// Token foi carregado, mas fica oculto até clicar em Mostrar Token
// Navegação sidebar
function navClick(e, url) {
e.preventDefault();
window.location.href = url;
}
function navClick(e, url) { e.preventDefault(); window.location.href = url; }
document.getElementById('navDashboard').onclick = function(e) { navClick(e, '/app/' + alias + '/dashboard'); };
document.getElementById('navClients').onclick = function(e) { navClick(e, '/app/' + alias + '/clients'); };
document.getElementById('navChat').onclick = function(e) { navClick(e, '/app/' + alias + '/company/' + (user.empresas?.[0] || 1) + '/conversation/0'); };
document.getElementById('navChat').onclick = function(e) { navClick(e, '/app/' + alias + '/company/' + (user.empresas && user.empresas[0] || 1) + '/conversation/0'); };
document.getElementById('navConfig').onclick = function(e) { navClick(e, '/app/' + alias + '/settings'); };
document.getElementById('navAllConvs').onclick = function(e) { navClick(e, '/app/' + alias + '/conversations/all'); };
document.getElementById('navRoutes').onclick = function(e) { navClick(e, '/app/' + alias + '/routes'); };
function copyToken() {
navigator.clipboard.writeText(token).then(() => {
alert('Token copiado!');
});
}
function logout() {
localStorage.removeItem('chatc2_token');
localStorage.removeItem('chatc2_alias');
localStorage.removeItem('chatc2_user');
['chatc2_token','chatc2_alias','chatc2_user'].forEach(function(k){ localStorage.removeItem(k); });
window.location.href = '/app/' + alias + '/login';
}
function showToken() {
const display = document.getElementById('tokenDisplay');
const btnShow = document.getElementById('btnShowToken');
const btnCopy = document.getElementById('btnCopyToken');
if (display.style.display === 'none') {
display.textContent = token;
display.style.display = 'block';
btnShow.textContent = 'Ocultar Token';
btnCopy.style.display = 'inline-block';
} else {
display.style.display = 'none';
btnShow.textContent = 'Mostrar Token';
btnCopy.style.display = 'none';
function esc(s){ return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
// ===== Carrega estatísticas =====
async function carregarStats() {
try {
var r = await (await fetch('/api/' + alias + '/dashboard/stats', {
headers: { 'Authorization': 'Bearer ' + token }
})).json();
if (!r.success) return;
var d = r.data;
document.getElementById('cAbertas').textContent = d.conversas.abertas;
document.getElementById('cNaoAtendidas').textContent = d.conversas.naoAtendidas;
document.getElementById('cNaoAtribuidas').textContent = d.conversas.naoAtribuidas;
document.getElementById('cPendentes').textContent = d.conversas.pendentes;
document.getElementById('aDisponiveis').textContent = d.atendentes.disponiveis;
document.getElementById('aDesconectados').textContent = d.atendentes.desconectados;
renderAtendentes(d.atendentes.lista);
renderHeatmap(d.trafego);
} catch(e) {}
}
function renderAtendentes(lista) {
var el = document.getElementById('atendentesLista');
if (!lista || lista.length === 0) {
el.innerHTML = '<div style="color:#9ca3af;font-size:13px">Nenhum atendente cadastrado.</div>';
return;
}
// Online primeiro
lista.sort(function(a,b){ return (b.online?1:0) - (a.online?1:0) || a.nome.localeCompare(b.nome); });
el.innerHTML = lista.map(function(a){
return '<div class="atendente-row">' +
'<span class="dot ' + (a.online ? 'on' : 'off') + '"></span>' +
'<span>' + esc(a.nome) + '</span>' +
'<span class="st">' + (a.online ? 'Disponível' : 'Desconectado') + '</span>' +
'</div>';
}).join('');
}
// ===== Heatmap estilo GitHub =====
function nivel(n) {
if (!n) return '';
if (n <= 2) return 'l1';
if (n <= 5) return 'l2';
if (n <= 10) return 'l3';
return 'l4';
}
function renderHeatmap(trafego) {
var mapa = {}; var totalGeral = 0;
(trafego || []).forEach(function(t){ mapa[t.dia] = t.total; totalGeral += t.total; });
var hoje = new Date(); hoje.setHours(0,0,0,0);
var inicio = new Date(hoje); inicio.setDate(inicio.getDate() - 364);
inicio.setDate(inicio.getDate() - inicio.getDay()); // alinha ao domingo
var meses = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
var grid = document.getElementById('heatmap');
var monthsBar = document.getElementById('hmMonths');
grid.innerHTML = ''; monthsBar.innerHTML = '';
var cursor = new Date(inicio);
var ultimoMes = -1;
while (cursor <= hoje) {
var col = document.createElement('div');
col.className = 'hm-col';
var mesDaColuna = cursor.getMonth();
// rótulo do mês quando muda no topo da coluna
var lbl = document.createElement('div');
lbl.style.width = '12px';
lbl.style.fontSize = '10px';
lbl.style.color = '#9ca3af';
if (mesDaColuna !== ultimoMes) { lbl.textContent = meses[mesDaColuna]; ultimoMes = mesDaColuna; }
else { lbl.innerHTML = '&nbsp;'; }
monthsBar.appendChild(lbl);
for (var dia = 0; dia < 7; dia++) {
var cell = document.createElement('div');
var iso = cursor.toISOString().split('T')[0];
if (cursor > hoje) { cell.className = 'hm-cell'; cell.style.visibility = 'hidden'; }
else {
var n = mapa[iso] || 0;
cell.className = 'hm-cell ' + nivel(n);
cell.title = iso.split('-').reverse().join('/') + ': ' + n + ' conversa' + (n === 1 ? '' : 's');
}
col.appendChild(cell);
cursor.setDate(cursor.getDate() + 1);
}
grid.appendChild(col);
}
document.getElementById('trafegoResumo').textContent =
totalGeral + ' conversa' + (totalGeral === 1 ? '' : 's') + ' iniciada' + (totalGeral === 1 ? '' : 's') + ' nos últimos 12 meses';
}
// ===== Presença + atualização periódica =====
function pingPresenca() {
fetch('/api/' + alias + '/dashboard/ping', {
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }
}).catch(function(){});
}
if (tipoChat === 'G') {
pingPresenca();
carregarStats();
setInterval(pingPresenca, 45000);
setInterval(carregarStats, 30000);
}
</script>
<script src="/js/dark-mode.js"></script>
+2
View File
@@ -702,6 +702,7 @@ async function carregarConfig() {
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgSaudacao" ' + (cfg.saudacaoAtiva === 'S' ? 'checked' : '') + '> <label for="cfgSaudacao">Ativar saudação automática</label></div></div>' +
'<div class="form-group"><label>Mensagem de Saudação</label><textarea id="cfgSaudacaoMsg">' + (cfg.saudacaoMensagem || '') + '</textarea></div>' +
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgNomeUser" ' + (cfg.enviarNomeUsuario === 'S' ? 'checked' : '') + '> <label for="cfgNomeUser">Mostrar nome do usuário nas mensagens ("Nome: Mensagem")</label></div></div>' +
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgEnviarBoleto" ' + (cfg.enviarBoleto === 'S' ? 'checked' : '') + '> <label for="cfgEnviarBoleto">💳 Permitir enviar boleto na conversa (botão "Enviar boleto")</label></div></div>' +
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgTriagem" ' + (cfg.triagemAtiva === 'S' ? 'checked' : '') + '> <label for="cfgTriagem">📋 Ativar fluxo de triagem (menu de opções)</label></div></div>' +
'<div class="form-group" id="triagemConfig" style="display:' + (cfg.triagemAtiva === 'S' ? 'block' : 'none') + ';padding:12px;background:#f9fafb;border-radius:8px;margin-bottom:12px">' +
'<div class="form-group"><label>Mensagem de boas-vindas (use {EMPRESA} para o nome)</label><textarea id="cfgTriagemWelcome" rows="2" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:13px;resize:vertical">' + (cfg.triagemMsgWelcome || '') + '</textarea></div>' +
@@ -739,6 +740,7 @@ window.salvarConfig = async function() {
saudacaoAtiva: document.getElementById('cfgSaudacao').checked ? 'S' : 'N',
saudacaoMensagem: document.getElementById('cfgSaudacaoMsg').value,
enviarNomeUsuario: document.getElementById('cfgNomeUser').checked ? 'S' : 'N',
enviarBoleto: document.getElementById('cfgEnviarBoleto').checked ? 'S' : 'N',
triagemAtiva: document.getElementById('cfgTriagem').checked ? 'S' : 'N',
triagemMsgWelcome: document.getElementById('cfgTriagemWelcome').value,
triagemMsgAfter: document.getElementById('cfgTriagemAfter').value,