ae629d1dc2
- Camada de banco unificada (src/database.js): drivers Postgres/Firebird, tradutor de SQL, suporte a schema e pool de conexões - Conexões: novo_local (Postgres externo) e firebird_local (legado) - Tela de rotas da API redesenhada (auth, params, exemplos de body) - Correções de segurança (críticos/altos/médios/baixos): XSS no chat, escalonamento de privilégio, mídia autenticada, SQL restrito a gerente, JWT sem fallback + issuer, IDOR em conversas, CORS por allowlist, rate-limit no login, limites de corpo por rota - Deploy alinhado: install.sh grava .env com PG_*, migracoes.js driver-aware Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
183 lines
8.9 KiB
HTML
183 lines
8.9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="pt-BR">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Conversas - Chatc2</title>
|
|
<link rel="stylesheet" href="/css/main.css">
|
|
<style>
|
|
body { background:#f3f4f6; display:flex; min-height:100vh; }
|
|
.sidebar-nav .admin-only { display:none; }
|
|
.main { flex:1; display:flex; flex-direction:column; min-width:0; }
|
|
.container { flex:1; padding:24px; overflow-y:auto; }
|
|
.filters { display:flex; gap:10px; margin-bottom:20px; flex-wrap:wrap; align-items:center; }
|
|
.filters select, .filters input { padding:9px 14px; border:2px solid #e5e7eb; border-radius:8px; font-size:13px; outline:none; background:#fff; color:#374151; transition:border-color .15s; }
|
|
.filters select:focus, .filters input:focus { border-color:#667eea; }
|
|
.filters button { padding:9px 16px; background:#667eea; color:#fff; border:none; border-radius:8px; font-size:13px; font-weight:600; cursor:pointer; transition:background .15s; }
|
|
.filters button:hover { background:#5a67d8; }
|
|
.status-badge { display:inline-block; padding:3px 10px; border-radius:10px; font-size:11px; font-weight:600; }
|
|
.status-E { background:#fef3c7; color:#92400e; }
|
|
.status-A { background:#d1fae5; color:#065f46; }
|
|
.status-F { background:#f3f4f6; color:#6b7280; }
|
|
.atender-btn { padding:5px 12px; border:none; border-radius:6px; background:#667eea; color:#fff; font-size:12px; font-weight:600; cursor:pointer; text-decoration:none; transition:background .15s; display:inline-block; }
|
|
.atender-btn:hover { background:#5a67d8; }
|
|
.loading { text-align:center; padding:40px; color:#9ca3af; }
|
|
</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'});}
|
|
window.darkModeApply=function(a){var e=document.body;if(!e)return;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'});};
|
|
window.darkModeIsDark=function(){return localStorage.getItem('chatc2_dark_mode')==='true';};</script>
|
|
</head>
|
|
<body>
|
|
<aside class="sidebar">
|
|
<div class="sidebar-brand"><div class="logo">C2</div><div><h2>Chatc2</h2><span id="sidebarAlias">-</span></div></div>
|
|
<nav class="sidebar-nav">
|
|
<div class="nav-label">Principal</div>
|
|
<a href="#" id="navDashboard"><span class="icon">📊</span> Dashboard</a>
|
|
<a href="#" id="navClients"><span class="icon">👥</span> Clientes</a>
|
|
<a href="#" class="active" id="navChat"><span class="icon">💬</span> Conversas</a>
|
|
<div class="nav-label admin-only" id="adminLabel">Administrador</div>
|
|
<a href="#" id="navConfig" class="admin-only"><span class="icon">⚙️</span> Configurações</a>
|
|
<a href="#" id="navRoutes" class="admin-only"><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></div>
|
|
</aside>
|
|
<div class="main">
|
|
<div class="topbar">
|
|
<span class="topbar-title">💬 Todas as Conversas</span>
|
|
<span id="totalInfo" style="font-size:13px;color:#6b7280"></span>
|
|
</div>
|
|
<div class="container">
|
|
<div class="filters">
|
|
<select id="filterStatus" onchange="carregar()">
|
|
<option value="A,E">Em aberto</option>
|
|
<option value="E">Em espera</option>
|
|
<option value="A">Em atendimento</option>
|
|
<option value="F">Finalizadas</option>
|
|
<option value="A,E,F">Todas</option>
|
|
</select>
|
|
<select id="filterAtribuicao" onchange="carregar()">
|
|
<option value="">Todas</option>
|
|
<option value="me">Minhas conversas</option>
|
|
<option value="sem">Sem atendente</option>
|
|
</select>
|
|
<input type="text" id="buscaContato" placeholder="Buscar por nome ou número..." onkeyup="if(event.key==='Enter')carregar()">
|
|
<button onclick="carregar()">🔄 Atualizar</button>
|
|
</div>
|
|
<div class="card" id="tabelaContainer">
|
|
<div class="loading"><div class="spinner"></div><p style="margin-top:8px">Carregando conversas...</p></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function(){
|
|
'use strict';
|
|
const token = localStorage.getItem('chatc2_token');
|
|
const pathParts = window.location.pathname.split('/');
|
|
const alias = pathParts[2] || localStorage.getItem('chatc2_alias') || 'lajedo';
|
|
localStorage.setItem('chatc2_alias', alias);
|
|
const user = JSON.parse(localStorage.getItem('chatc2_user') || '{}');
|
|
const empresaId = user.empresas?.[0] || 1;
|
|
|
|
if (!token) { window.location.href = '/app/' + alias + '/login'; return; }
|
|
|
|
document.getElementById('sidebarAlias').textContent = alias;
|
|
|
|
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) { e.preventDefault(); carregar(); };
|
|
document.getElementById('navConfig').onclick = function(e) { navClick(e, '/app/'+alias+'/settings'); };
|
|
document.getElementById('navRoutes').onclick = function(e) { navClick(e, '/app/'+alias+'/routes'); };
|
|
|
|
const tc = user.tipoChat || 'A';
|
|
if (tc === 'G') {
|
|
document.querySelectorAll('.admin-only').forEach(function(el) { el.style.display = ''; });
|
|
}
|
|
|
|
window.logout = function() {
|
|
['chatc2_token','chatc2_alias','chatc2_user'].forEach(function(k) { localStorage.removeItem(k); });
|
|
window.location.href = '/app/' + alias + '/login';
|
|
};
|
|
|
|
function api(path) {
|
|
return fetch('/api/' + alias + path, { headers: { 'Authorization': 'Bearer ' + token } }).then(function(r) { return r.json(); });
|
|
}
|
|
|
|
window.carregar = async function() {
|
|
const container = document.getElementById('tabelaContainer');
|
|
container.innerHTML = '<div class="loading"><div class="spinner"></div><p style="margin-top:8px">Carregando conversas...</p></div>';
|
|
|
|
const status = document.getElementById('filterStatus').value;
|
|
const atrib = document.getElementById('filterAtribuicao').value;
|
|
let busca = document.getElementById('buscaContato').value.trim();
|
|
|
|
const data = await api('/conversations?empresaId=' + empresaId + '&status=' + status);
|
|
|
|
if (!data.success) {
|
|
container.innerHTML = '<div class="empty-state"><div class="icon">⚠️</div><p>' + data.error + '</p></div>';
|
|
return;
|
|
}
|
|
|
|
let convs = data.data || [];
|
|
|
|
// Filtro de atribuição
|
|
if (atrib === 'me') {
|
|
convs = convs.filter(function(c) { return c.usuarioId === user.id; });
|
|
} else if (atrib === 'sem') {
|
|
convs = convs.filter(function(c) { return !c.usuarioId; });
|
|
}
|
|
|
|
// Filtro de busca
|
|
if (busca) {
|
|
const q = busca.toLowerCase();
|
|
convs = convs.filter(function(c) {
|
|
return (c.nomeContato && c.nomeContato.toLowerCase().includes(q)) ||
|
|
(c.numero && c.numero.includes(q));
|
|
});
|
|
}
|
|
|
|
document.getElementById('totalInfo').textContent = convs.length + ' conversa(s)';
|
|
|
|
if (convs.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><div class="icon">💬</div><p>Nenhuma conversa encontrada</p></div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<table><thead><tr><th>Contato</th><th>Número</th><th style="width:100px">Status</th><th>Atendente</th><th>Última msg</th><th style="width:80px">Ação</th></tr></thead><tbody>';
|
|
|
|
convs.forEach(function(c) {
|
|
const statusLabel = c.status === 'E' ? '🟡 Espera' : c.status === 'A' ? '🟢 Atendimento' : '⚫ Finalizado';
|
|
const labelsHtml = (c.labels || []).map(function(l) {
|
|
return '<span style="display:inline-block;padding:0 6px;border-radius:3px;background:'+(l.cor||'#667eea')+';color:#fff;font-size:10px;margin:1px">'+(l.nome||'')+'</span>';
|
|
}).join('');
|
|
const ultima = (c.ultimaMsg || '').substring(0, 60);
|
|
const usuarioNome = c.usuarioId ? 'Usuário ' + c.usuarioId : '-';
|
|
|
|
html += '<tr>' +
|
|
'<td><strong>' + (c.nomeContato || 'Desconhecido') + '</strong>' + (labelsHtml ? '<br>' + labelsHtml : '') + '</td>' +
|
|
'<td>' + (c.numero || '-') + '</td>' +
|
|
'<td><span class="status-badge status-' + c.status + '">' + statusLabel + '</span></td>' +
|
|
'<td style="font-size:12px">' + usuarioNome + '</td>' +
|
|
'<td style="font-size:12px;color:#6b7280;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + ultima + '</td>' +
|
|
'<td><a href="/app/' + alias + '/company/' + empresaId + '/conversation/' + c.id + '" class="atender-btn">Atender</a></td>' +
|
|
'</tr>';
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
container.innerHTML = html;
|
|
};
|
|
|
|
carregar();
|
|
|
|
// Auto refresh
|
|
setInterval(carregar, 10000);
|
|
|
|
})();
|
|
</script>
|
|
<script src="/js/dark-mode.js"></script>
|
|
</body>
|
|
</html>
|