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';
|
||||
|
||||
@@ -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
@@ -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 vê 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
// ===== 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 = ' '; }
|
||||
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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user