Migração para PostgreSQL multi-driver + correções de segurança

- 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>
This commit is contained in:
2026-06-17 10:02:59 -03:00
commit ae629d1dc2
50 changed files with 17137 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
<!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>
+1548
View File
File diff suppressed because it is too large Load Diff
+741
View File
@@ -0,0 +1,741 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Detalhes do Cliente - 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; }
.loading { text-align:center; padding:80px 20px; color:#9ca3af; }
.error-box { background:#fef2f2; border:1px solid #fecaca; border-radius:12px; padding:32px; text-align:center; color:#991b1b; }
.error-box .icon { font-size:48px; margin-bottom:12px; }
.client-header { background:#fff; border-radius:14px; padding:24px; box-shadow:0 1px 4px rgba(0,0,0,0.08); margin-bottom:20px; display:flex; align-items:center; gap:20px; border:1px solid #f3f4f6; }
.client-avatar { width:68px; height:68px; border-radius:50%; background:linear-gradient(135deg,#667eea,#764ba2); display:flex; align-items:center; justify-content:center; color:#fff; font-size:26px; font-weight:800; flex-shrink:0; box-shadow:0 4px 14px rgba(102,126,234,0.3); }
.client-header-info h1 { font-size:22px; font-weight:800; color:#111827; margin-bottom:4px; letter-spacing:-0.3px; }
.client-header-info .matricula { font-size:14px; color:#6b7280; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:18px; margin-bottom:20px; }
.card h3 { font-size:12px; text-transform:uppercase; letter-spacing:0.07em; color:#9ca3af; margin-bottom:14px; font-weight:700; }
.field { margin-bottom:12px; }
.field:last-child { margin-bottom:0; }
.field .label { font-size:11px; text-transform:uppercase; letter-spacing:0.06em; color:#9ca3af; margin-bottom:3px; font-weight:600; }
.field .value { font-size:15px; color:#111827; font-weight:500; }
.carnes-section { margin-top:20px; }
.carnes-filters { display:flex; gap:10px; margin-bottom:16px; flex-wrap:wrap; align-items:center; }
.carnes-filters label { display:flex; align-items:center; gap:6px; font-size:13px; color:#374151; cursor:pointer; padding:7px 14px; border-radius:8px; border:1px solid #e5e7eb; background:#fff; transition:all 0.15s; user-select:none; font-weight:500; }
.carnes-filters label:hover { border-color:#667eea; color:#667eea; }
.carnes-filters label.filtro-ativo { border-color:#667eea; background:#eef2ff; color:#667eea; font-weight:600; }
.carnes-filters label input { accent-color:#667eea; }
table { font-size:13px; }
.valor { text-align:right; font-family:'SF Mono',monospace; }
.centro { text-align:center; }
.sit-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; font-weight:600; }
.vencido-dot { display:inline-block; width:8px; height:8px; border-radius:50%; background:#ef4444; margin-right:4px; }
.back-btn { margin-right:14px; padding:7px 14px; background:#f3f4f6; border:1px solid #e5e7eb; border-radius:8px; font-size:13px; font-weight:500; cursor:pointer; color:#374151; text-decoration:none; transition:all .15s; display:inline-flex; align-items:center; gap:4px; }
.back-btn:hover { background:#e5e7eb; border-color:#d1d5db; }
.empty-msg { text-align:center; padding:32px; color:#9ca3af; font-size:14px; }
</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="#" class="active" id="navClients"><span class="icon">👥</span> Clientes</a>
<a href="#" id="navChatDetail"><span class="icon">💬</span> Conversas</a>
<div class="nav-label admin-only">Administrador</div>
<a href="#" id="navConfigDetail" class="admin-only"><span class="icon">⚙️</span> Configurações</a>
<a href="#" id="navRoutesDetail" 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">
<a href="#" class="back-btn" id="backBtn">← Voltar</a>
<span class="topbar-title">Detalhes do Cliente</span>
<a href="#" class="back-btn" id="btnIniciarConv" style="font-size:13px;margin-left:auto" title="Iniciar conversa WhatsApp">💬 Iniciar Conversa</a>
</div>
<div class="container" id="container">
<div class="loading" id="loadingState">
<div class="spinner"></div>
<p style="margin-top:12px">Carregando dados do cliente...</p>
</div>
</div>
</div>
<!-- Modal Nova Conversa -->
<div class="modal-overlay" id="modalNovaConvDet" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:450px">
<h3 style="margin-bottom:16px">💬 Iniciar Conversa</h3>
<input type="hidden" id="convClienteIdDet">
<input type="hidden" id="convEmpresaIdDet">
<div class="form-group"><label>Número</label><input type="text" id="convNumeroDet" readonly style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;background:#f9fafb;margin-bottom:12px"></div>
<div class="form-group"><label>Cliente</label><input type="text" id="convNomeDet" readonly style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;background:#f9fafb;margin-bottom:12px"></div>
<div class="form-group"><label>Instância WhatsApp</label><select id="convInstanciaDet" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;margin-bottom:12px"></select></div>
<div class="form-group"><label>Mensagem inicial</label><textarea id="convMensagemDet" rows="3" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;resize:vertical;margin-bottom:12px" placeholder="Digite a mensagem que será enviada..."></textarea></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button onclick="document.getElementById('modalNovaConvDet').style.display='none'" style="padding:8px 16px;border:1px solid #d1d5db;border-radius:8px;background:#fff;cursor:pointer">Cancelar</button>
<button onclick="iniciarConversaDet()" style="padding:8px 16px;background:#667eea;color:#fff;border:none;border-radius:8px;cursor:pointer">💬 Enviar e Abrir Chat</button>
</div>
</div>
</div>
<script>
(function() {
'use strict';
// ---- Dados da URL ----
var pathParts = window.location.pathname.split('/');
var alias = pathParts[2];
var empresaId = pathParts[4];
var clienteId = pathParts[6];
// ---- Token ----
var token = localStorage.getItem('chatc2_token');
if (!token) { window.location.href = '/app/' + alias + '/login'; return; }
localStorage.setItem('chatc2_alias', alias);
// ---- Elementos ----
var container = document.getElementById('container');
var sidebarAlias = document.getElementById('sidebarAlias');
var backBtn = document.getElementById('backBtn');
sidebarAlias.textContent = alias;
backBtn.href = '/app/' + alias + '/clients';
// Botão iniciar conversa - abre modal
document.getElementById('btnIniciarConv').onclick = function(e) {
e.preventDefault();
// Pega dados do cliente do último load (cache na variável global)
var celular = window._clienteData?.celular?.original || window._clienteData?.celular || '';
var nome = window._clienteData?.nome || '';
abrirModalConversa(parseInt(empresaId), parseInt(clienteId), nome, celular.replace(/\D/g,''));
};
// ---- Navegação ----
document.getElementById("navDashboard").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/dashboard"; };
document.getElementById("navClients").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/clients"; };
document.getElementById("navChatDetail").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/company/" + empresaId + "/conversation/0"; };
document.getElementById("navConfigDetail").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/settings"; };
document.getElementById("navRoutesDetail").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/routes"; };
var userDetail = JSON.parse(localStorage.getItem('chatc2_user') || '{}');
var tc = userDetail.tipoChat || "A";
if (tc === "G") {
var admins = document.querySelectorAll(".admin-only");
for (var i = 0; i < admins.length; i++) admins[i].style.display = "";
}
window.logout = function() {
localStorage.removeItem('chatc2_token');
localStorage.removeItem('chatc2_alias');
localStorage.removeItem('chatc2_user');
window.location.href = '/app/' + alias + '/login';
};
// ---- Utilitário fetch com token ----
function apiFetch(url) {
return fetch(url, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) {
if (r.status === 401 || r.status === 403) {
localStorage.removeItem('chatc2_token');
localStorage.removeItem('chatc2_user');
window.location.href = '/app/' + alias + '/login';
throw new Error('redirect');
}
return r.json();
});
}
// ---- Formata moeda ----
function fmtMoney(v) { return 'R$ ' + (v || 0).toFixed(2).replace('.', ','); }
// ---- Formata data ----
function fmtDate(d) {
if (!d) return '-';
var date = new Date(d + (d.includes('T') ? '' : 'T12:00:00'));
return date.toLocaleDateString('pt-BR');
}
// ============================================================
// CARREGAR CLIENTE
// ============================================================
function loadClient() {
apiFetch('/api/' + alias + '/company/' + empresaId + '/client/' + clienteId)
.then(function(data) {
if (!data.success) {
container.innerHTML = '<div class="error-box"><div class="icon">⚠️</div><h2>Erro</h2><p>' + data.error + '</p></div>';
return;
}
renderClient(data.data);
carregarConversas();
// Verifica se o modulo estoque esta ativo para esconder a aba convalescentes
fetch('/api/' + alias + '/clients/' + clienteId + '/convalescentes', {
headers: { 'Authorization': 'Bearer ' + token }
}).then(function(r){ return r.json(); }).then(function(d) {
var btnC = document.getElementById('abaConvalescentes');
if (btnC && d.moduloInativo === true) {
btnC.style.display = 'none';
}
}).catch(function(){});
})
.catch(function(err) {
if (err.message === 'redirect') return;
container.innerHTML = '<div class="error-box"><div class="icon">⚠️</div><h2>Erro de conexão</h2><p>' + err.message + '</p></div>';
});
}
// Guarda dados do cliente para uso em outros lugares
window._clienteData = null;
function renderClient(c) {
window._clienteData = c;
var sitClass = c.situacao && c.situacao.codigo === 'A' ? 'ativo' : 'inativo';
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() : '?';
container.innerHTML =
'<div class="client-header">' +
'<div class="client-avatar">' + iniciais + '</div>' +
'<div class="client-header-info">' +
'<h1>' + (c.nome || '-') + '</h1>' +
'<div class="matricula">Matrícula: ' + (c.matricula || '-') + '</div>' +
'</div>' +
'<span class="situacao-badge ' + sitClass + '">' + sitLabel + '</span>' +
'</div>' +
'<div class="grid">' +
'<div class="card"><h3>📞 Contato <button onclick="editarContato()" style="float:right;padding:2px 8px;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer;font-size:11px">✏️ Editar</button></h3>' +
'<div class="field"><div class="label">Email</div><div class="value" id="campoEmail">' + (c.email || '-') + '</div></div>' +
'<div class="field"><div class="label">Celular</div><div class="value" id="campoCelular">' + (c.celular && c.celular.formatado ? c.celular.formatado : c.celular && c.celular.original ? c.celular.original : '-') + '</div></div>' +
'<div class="field"><div class="label">Telefone</div><div class="value" id="campoTelefone">' + (c.telefone || '-') + '</div></div>' +
'<div id="editContatoForm" style="display:none;margin-top:12px;padding:12px;background:#f9fafb;border-radius:8px">' +
'<input type="email" id="editEmail" placeholder="Email" style="width:100%;padding:8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;margin-bottom:8px">' +
'<input type="text" id="editCelular" placeholder="Celular" style="width:100%;padding:8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;margin-bottom:8px">' +
'<input type="text" id="editTelefone" placeholder="Telefone" style="width:100%;padding:8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;margin-bottom:8px">' +
'<div style="display:flex;gap:8px">' +
'<button onclick="salvarContato()" style="padding:6px 14px;background:#667eea;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px">💾 Salvar</button>' +
'<button onclick="cancelarEditarContato()" style="padding:6px 14px;border:1px solid #d1d5db;border-radius:6px;background:#fff;cursor:pointer;font-size:12px">Cancelar</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="card"><h3>📍 Endereço</h3>' +
'<div class="field"><div class="label">Logradouro</div><div class="value">' + (c.enderecoFaturamento && c.enderecoFaturamento.logradouro || '-') + '</div></div>' +
'<div class="field"><div class="label">Número</div><div class="value">' + (c.enderecoFaturamento && c.enderecoFaturamento.numero || '-') + '</div></div>' +
'<div class="field"><div class="label">Complemento</div><div class="value">' + (c.enderecoFaturamento && c.enderecoFaturamento.complemento || '-') + '</div></div>' +
'<div class="field"><div class="label">CEP</div><div class="value">' + (c.enderecoFaturamento && c.enderecoFaturamento.cep || '-') + '</div></div>' +
'</div>' +
'<div class="card"><h3>🏙️ Localização</h3>' +
'<div class="field"><div class="label">Cidade</div><div class="value">' + (c.cidade && c.cidade.nome || '-') + '</div></div>' +
'<div class="field"><div class="label">Bairro</div><div class="value">' + (c.bairro && c.bairro.nome || '-') + '</div></div>' +
'</div>' +
'<div class="card"><h3>💰 Cobrança</h3>' +
'<div class="field"><div class="label">Cobrador</div><div class="value">' + (c.cobrador && c.cobrador.nome || 'Não definido') + '</div></div>' +
'<div class="field"><div class="label">Dia</div><div class="value">' + (c.diaCobranca ? c.diaCobranca + 'º dia' : 'Não definido') + '</div></div>' +
'</div>' +
'</div>' +
// ABAS: Conversas | Títulos | Dependentes | Convalescentes
'<div style="display:flex;gap:0;margin-bottom:16px;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.08)">' +
'<button class="aba-ativa" onclick="mudarAba(\'conversas\')" id="abaConversas" style="flex:1;padding:10px;border:none;background:#667eea;color:#fff;font-size:13px;font-weight:600;cursor:pointer">💬 Conversas</button>' +
'<button onclick="mudarAba(\'titulos\')" id="abaTitulos" style="flex:1;padding:10px;border:none;background:#f3f4f6;color:#374151;font-size:13px;cursor:pointer">📄 Títulos</button>' +
'<button onclick="mudarAba(\'dependentes\')" id="abaDependentes" style="flex:1;padding:10px;border:none;background:#f3f4f6;color:#374151;font-size:13px;cursor:pointer">👥 Dependentes</button>' +
'<button onclick="mudarAba(\'convalescentes\')" id="abaConvalescentes" style="flex:1;padding:10px;border:none;background:#f3f4f6;color:#374151;font-size:13px;cursor:pointer">🛏️ Convalescentes</button>' +
'</div>' +
'<div id="conteudoAba">' +
// CONVERSAS (padrão - primeira aba)
'<div class="card" id="conteudoConversas">' +
'<h3>💬 Histórico de Conversas</h3>' +
'<div id="conversasArea"><div class="loading" style="padding:20px"><div class="spinner"></div><p style="margin-top:8px">Carregando histórico de conversas...</p></div></div>' +
'</div>' +
// TÍTULOS
'<div class="carnes-section card" id="conteudoTitulos" style="display:none">' +
'<h3>📄 Títulos</h3>' +
'<div class="carnes-filters" id="carnesFilters">' +
'<label class="filtro-ativo" data-tipo="abertos"><input type="checkbox" checked onchange="filtroClick(this)"> Abertos (<span id="cntAbertos">0</span>)</label>' +
'<label data-tipo="baixados"><input type="checkbox" onchange="filtroClick(this)"> Baixados (<span id="cntBaixados">0</span>)</label>' +
'<label data-tipo="parcial"><input type="checkbox" onchange="filtroClick(this)"> Parcial (<span id="cntParcial">0</span>)</label>' +
'<label class="filtro-ativo" data-tipo="vencidos" style="border-color:#fca5a5;"><input type="checkbox" checked onchange="filtroClick(this)"> 🔴 Vencidos (<span id="cntVencidos">0</span>)</label>' +
'</div>' +
'<div id="carnesArea"><div class="loading" style="padding:20px"><div class="spinner"></div><p style="margin-top:8px">Carregando títulos...</p></div></div>' +
'</div>' +
// DEPENDENTES
'<div class="card" id="conteudoDependentes" style="display:none">' +
'<h3>👥 Dependentes</h3>' +
'<div id="dependentesArea"><div class="loading" style="padding:20px"><div class="spinner"></div><p style="margin-top:8px">Carregando dependentes...</p></div></div>' +
'</div>' +
// CONVALESCENTES
'<div class="card" id="conteudoConvalescentes" style="display:none">' +
'<h3>🛏️ Convalescentes</h3>' +
'<div id="convalescentesArea"><div class="loading" style="padding:20px"><div class="spinner"></div><p style="margin-top:8px">Carregando convalescentes...</p></div></div>' +
'</div>'
'</div>'; // fecha conteudoAba
}
function getParentescoLabel(cod) {
var map = {'01':'Conjugue','02':'Companheiro(a)','03':'Filho(a)','04':'Enteado(a)','05':'Esposo(a)','06':'Excluído','07':'Neto(a)','08':'Pai','09':'Mãe','10':'Sobrinho(a)','11':'Tio(a)','12':'Primo(a)','13':'Irmão(ã)','14':'Sogro(a)','15':'Cunhado(a)','16':'Outros','17':'Agregado','18':'Genro/Nora','19':'Avô(ó)'};
return map[cod] || cod;
}
window.mudarAba = function(aba) {
var btnV = document.getElementById('abaConversas');
var btnT = document.getElementById('abaTitulos');
var btnD = document.getElementById('abaDependentes');
var btnC = document.getElementById('abaConvalescentes');
var cV = document.getElementById('conteudoConversas');
var cT = document.getElementById('conteudoTitulos');
var cD = document.getElementById('conteudoDependentes');
var cC = document.getElementById('conteudoConvalescentes');
var btns = [btnV, btnT, btnD, btnC];
var contents = [cV, cT, cD, cC];
btns.forEach(function(b) { if (b) { b.style.background = '#f3f4f6'; b.style.color = '#374151'; b.style.fontWeight = '400'; } });
contents.forEach(function(c) { if (c) c.style.display = 'none'; });
if (aba === 'conversas') {
btnV.style.background = '#667eea'; btnV.style.color = '#fff'; btnV.style.fontWeight = '600';
cV.style.display = 'block';
carregarConversas();
} else if (aba === 'titulos') {
btnT.style.background = '#667eea'; btnT.style.color = '#fff'; btnT.style.fontWeight = '600';
cT.style.display = 'block';
loadCarnes(1);
} else if (aba === 'dependentes') {
btnD.style.background = '#667eea'; btnD.style.color = '#fff'; btnD.style.fontWeight = '600';
cD.style.display = 'block';
carregarDependentes();
} else if (aba === 'convalescentes') {
btnC.style.background = '#667eea'; btnC.style.color = '#fff'; btnC.style.fontWeight = '600';
cC.style.display = 'block';
carregarConvalescentes();
}
};
async function carregarConversas() {
var area = document.getElementById('conversasArea');
area.innerHTML = '<div class="loading" style="padding:20px"><div class="spinner"></div><p style="margin-top:8px">Carregando histórico...</p></div>';
try {
var res = await fetch('/api/' + alias + '/clients/' + clienteId + '/conversations', {
headers: { 'Authorization': 'Bearer ' + token }
});
var data = await res.json();
if (!data.success || !data.data || data.data.length === 0) {
area.innerHTML = '<div class="empty-msg">🔍 Nenhuma conversa encontrada para este cliente</div>';
return;
}
var html = '<div style="overflow-x:auto"><table><thead><tr>' +
'<th>Data</th><th>Contato</th><th>Tipo</th><th>Status</th><th>Atendente</th><th>Equipe</th><th></th>' +
'</tr></thead><tbody>';
data.data.forEach(function(cv) {
var dataStr = cv.dtUltimaMsg ? new Date(cv.dtUltimaMsg + (cv.dtUltimaMsg.includes('T') ? '' : 'T12:00:00')).toLocaleDateString('pt-BR') + ' ' + new Date(cv.dtUltimaMsg + (cv.dtUltimaMsg.includes('T') ? '' : 'T12:00:00')).toLocaleTimeString('pt-BR', {hour:'2-digit',minute:'2-digit'}) : '-';
var quem = cv.quemFalou || cv.nomeContato || cv.numero || '-';
var tipoPessoa = cv.isTitular ? '👤 Titular' : (cv.parentesco ? '👥 ' + cv.parentesco : '👥 Dependente');
var statusLabel = cv.status === 'A' ? '🟢 Aberta' : cv.status === 'E' ? '🟡 Em espera' : cv.status === 'F' ? '⚫ Finalizada' : cv.status;
var usuario = cv.usuarioNome || (cv.usuarioId ? '#' + cv.usuarioId : '-');
var equipe = cv.equipeNome || (cv.equipeId ? '#' + cv.equipeId : '-');
html += '<tr style="cursor:pointer" onclick="window.open(\'/app/' + alias + '/company/' + cv.empresaId + '/conversation/' + cv.id + '\', \'_blank\')">' +
'<td style="white-space:nowrap;font-size:12px">' + dataStr + '</td>' +
'<td><strong>' + quem + '</strong></td>' +
'<td style="font-size:12px">' + tipoPessoa + '</td>' +
'<td style="font-size:12px">' + statusLabel + '</td>' +
'<td style="font-size:12px">' + usuario + '</td>' +
'<td style="font-size:12px">' + equipe + '</td>' +
'<td><span style="color:#667eea;font-size:12px">Abrir →</span></td>' +
'</tr>';
});
html += '</tbody></table></div>';
area.innerHTML = html;
} catch(err) {
area.innerHTML = '<div class="error-box" style="padding:16px">Erro ao carregar histórico: ' + err.message + '</div>';
}
}
async function carregarDependentes() {
var area = document.getElementById('dependentesArea');
area.innerHTML = '<div class="loading" style="padding:20px"><div class="spinner"></div><p style="margin-top:8px">Carregando...</p></div>';
try {
var res = await fetch('/api/' + alias + '/clients/' + clienteId + '/dependents', {
headers: { 'Authorization': 'Bearer ' + token }
});
var data = await res.json();
if (!data.success || !data.data || data.data.length === 0) {
area.innerHTML = '<div class="empty-msg">🔍 Nenhum dependente encontrado</div>';
return;
}
var html = '<div style="overflow-x:auto"><table><thead><tr><th>Nome</th><th>Parentesco</th><th>Telefone</th><th>Adicional</th><th>Valor</th><th>Situação</th></tr></thead><tbody>';
data.data.forEach(function(d) {
var sit = d.situacao === 'A' ? '<span class="situacao-badge ativo" style="font-size:11px">Ativo</span>' : '<span class="situacao-badge inativo" style="font-size:11px">Inativo</span>';
var adicional = d.adicional === 'S' ? '✅ Sim' : '❌ Não';
var telEdit = d.telefone || '';
html += '<tr><td><strong>' + d.nome + '</strong></td><td>' + getParentescoLabel(d.parentesco) + '</td><td>' +
'<span id="depTel_' + d.id + '">' + (telEdit || '-') + '</span> ' +
'<button onclick="editarTelDep(' + d.id + ')" data-tel="' + telEdit.replace(/"/g,'&quot;') + '" style="padding:1px 6px;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer;font-size:10px">✏️</button></td><td>' + adicional + '</td><td style="text-align:right;font-family:monospace">R$ ' + (d.valorContribuicao / 100).toFixed(2) + '</td><td>' + sit + '</td></tr>';
});
html += '</tbody></table></div>';
area.innerHTML = html;
} catch(e) {
area.innerHTML = '<div class="empty-msg">⚠️ Erro ao carregar: ' + e.message + '</div>';
}
}
async function carregarConvalescentes() {
var area = document.getElementById('convalescentesArea');
area.innerHTML = '<div class="loading" style="padding:20px"><div class="spinner"></div><p style="margin-top:8px">Carregando...</p></div>';
try {
var res = await fetch('/api/' + alias + '/clients/' + clienteId + '/convalescentes', {
headers: { 'Authorization': 'Bearer ' + token }
});
var data = await res.json();
if (!data.success || !data.data || data.data.length === 0) {
area.innerHTML = '<div class="empty-msg">🛏️ Nenhum convalescente encontrado</div>';
return;
}
var html = '';
data.data.forEach(function(c) {
var sitLabel = c.situacao || '-';
var sitBadge = c.situacao === 'D' ? '<span class="sit-badge" style="background:#d1fae5;color:#065f46">Devolvido</span>' :
c.situacao === 'E' ? '<span class="sit-badge" style="background:#fef3c7;color:#92400e">Entregue</span>' :
c.situacao === 'A' ? '<span class="sit-badge" style="background:#dbeafe;color:#1e40af">Ativo</span>' :
c.situacao === 'C' ? '<span class="sit-badge" style="background:#fef2f2;color:#991b1b">Cancelado</span>' :
'<span class="sit-badge" style="background:#f3f4f6;color:#6b7280">' + c.situacao + '</span>';
html += '<div style="border:1px solid #e5e7eb;border-radius:8px;padding:16px;margin-bottom:12px">' +
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">' +
'<h4 style="margin:0;font-size:15px">🛏️ Convalescente #' + c.id + '</h4>' +
sitBadge +
'</div>' +
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px;font-size:13px;margin-bottom:12px">' +
'<div><span style="color:#9ca3af">Valor Total:</span> <strong>R$ ' + (c.valorTotal || 0).toFixed(2) + '</strong></div>' +
'<div><span style="color:#9ca3af">Data Saída:</span> ' + fmtDate(c.dtSaida) + '</div>' +
'<div><span style="color:#9ca3af">Previsão Retorno:</span> ' + fmtDate(c.dtPrevisaoRetorno) + '</div>' +
(c.dtCancelou ? '<div><span style="color:#9ca3af">Data Cancelamento:</span> ' + fmtDate(c.dtCancelou) + '</div>' : '') +
'<div><span style="color:#9ca3af">Cadastrado por:</span> ' + (c.usuarioNome || '-') + '</div>' +
'</div>';
// SUB-ABAS: Boletos | Itens
var temCarnes = c.carnes && c.carnes.length > 0;
var temItens = c.itens && c.itens.length > 0;
if (temCarnes || temItens) {
html += '<div style="margin-top:12px;padding-top:12px;border-top:1px solid #e5e7eb">' +
'<div style="display:flex;gap:0;margin-bottom:8px;border-radius:6px;overflow:hidden">';
if (temCarnes) {
html += '<button class="subaba-ativa" onclick="mudarSubaba(\'carnes_' + c.id + '\',this)" id="subCarnes_' + c.id + '" style="flex:1;padding:6px 10px;border:none;background:#667eea;color:#fff;font-size:11px;font-weight:600;cursor:pointer">📄 Boletos (' + c.carnes.length + ')</button>';
}
if (temItens) {
html += '<button onclick="mudarSubaba(\'itens_' + c.id + '\',this)" id="subItens_' + c.id + '" style="flex:1;padding:6px 10px;border:none;background:#f3f4f6;color:#374151;font-size:11px;cursor:pointer">📦 Itens (' + c.itens.length + ')</button>';
}
html += '</div>';
// Conteudo Boletos
if (temCarnes) {
html += '<div id="conteudoCarnes_' + c.id + '" style="overflow-x:auto"><table style="font-size:12px"><thead><tr>' +
'<th>Vencimento</th><th style="text-align:right">Valor</th><th>Situação</th>' +
'</tr></thead><tbody>';
c.carnes.forEach(function(b) {
html += '<tr>' +
'<td>' + fmtDate(b.vencimento) + '</td>' +
'<td style="text-align:right;font-family:monospace">R$ ' + (b.valorParcela || 0).toFixed(2) + '</td>' +
'<td><span class="sit-badge" style="background:' + b.situacao.color + ';color:' + b.situacao.textColor + '">' + b.situacao.label + '</span></td>' +
'</tr>';
});
html += '</tbody></table></div>';
} else {
html += '<div id="conteudoCarnes_' + c.id + '" style="display:none"></div>';
}
// Conteudo Itens
html += '<div id="conteudoItens_' + c.id + '" style="' + (temCarnes && !temItens ? 'display:none' : '') + ';overflow-x:auto">';
if (temItens) {
html += '<table style="font-size:12px"><thead><tr>' +
'<th>Produto</th><th style="text-align:center">Quantidade</th>' +
'</tr></thead><tbody>';
c.itens.forEach(function(item) {
html += '<tr>' +
'<td>' + (item.produto || '-') + '</td>' +
'<td style="text-align:center;font-weight:600">' + (item.quantidade || 0) + '</td>' +
'</tr>';
});
html += '</tbody></table>';
} else {
html += '<div class="empty-msg" style="padding:12px;font-size:12px">📦 Nenhum item vinculado</div>';
}
html += '</div></div>';
}
html += '</div>';
});
area.innerHTML = html;
} catch(e) {
area.innerHTML = '<div class="empty-msg">⚠️ Erro: ' + e.message + '</div>';
}
}
// Alterna entre sub-abas (Boletos / Itens) dentro de cada convalescente
window.mudarSubaba = function(id, btn) {
var container = btn.parentElement.parentElement;
var botoes = container.querySelectorAll('button[id^="sub"]');
botoes.forEach(function(b) { b.style.background = '#f3f4f6'; b.style.color = '#374151'; });
btn.style.background = '#667eea'; btn.style.color = '#fff';
var prefixo = id.split('_')[0];
var covId = id.split('_')[1];
var cCarnes = document.getElementById('conteudoCarnes_' + covId);
var cItens = document.getElementById('conteudoItens_' + covId);
if (cCarnes) cCarnes.style.display = prefixo === 'carnes' ? '' : 'none';
if (cItens) cItens.style.display = prefixo === 'itens' ? '' : 'none';
};
window.editarTelDep = function(id) {
var btn = document.querySelector('button[onclick="editarTelDep(' + id + ')"]');
var telAtual = btn ? (btn.getAttribute('data-tel') || '') : '';
var novo = prompt('Editar telefone do dependente:', telAtual || '');
if (novo === null || novo.trim() === telAtual) return;
fetch('/api/' + alias + '/dependents/' + id + '/phone', {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ telefone: novo.trim() })
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.success) { document.getElementById('depTel_' + id).textContent = novo.trim(); alert('Telefone atualizado!'); }
else alert('Erro: ' + d.error);
}).catch(function(e) { alert('Erro: ' + e.message); });
};
// ============================================================
// CARNES / TÍTULOS
// ============================================================
var carnesPage = 1;
// Chamado sempre que um checkbox de filtro é marcado/desmarcado
window.filtroClick = function(checkbox) {
var label = checkbox.closest('label');
if (checkbox.checked) {
label.classList.add('filtro-ativo');
} else {
label.classList.remove('filtro-ativo');
}
carnesPage = 1;
loadCarnes();
};
function getTiposAtivos() {
var tipos = [];
var labels = document.querySelectorAll('#carnesFilters label[data-tipo]');
for (var i = 0; i < labels.length; i++) {
if (labels[i].classList.contains('filtro-ativo')) {
tipos.push(labels[i].getAttribute('data-tipo'));
}
}
return tipos;
}
// Torna global para os onclick dos botões de paginação
window.loadCarnes = function(page) {
if (page) carnesPage = page;
var area = document.getElementById('carnesArea');
if (!area) return;
var tipos = getTiposAtivos();
if (tipos.length === 0) {
area.innerHTML = '<div class="empty-msg">✅ Selecione ao menos um filtro</div>';
return;
}
var params = 'page=' + carnesPage + '&limit=20&tipo=' + tipos.join(',');
area.innerHTML = '<div class="loading" style="padding:20px"><div class="spinner"></div><p style="margin-top:8px">Carregando títulos...</p></div>';
apiFetch('/api/' + alias + '/clients/' + clienteId + '/carnes?' + params)
.then(function(data) {
if (!data.success) {
area.innerHTML = '<div class="empty-msg">⚠️ ' + data.error + '</div>';
return;
}
// Atualiza contagens nos filtros
if (data.contagens) {
document.getElementById('cntAbertos').textContent = data.contagens.abertos || 0;
document.getElementById('cntBaixados').textContent = data.contagens.baixados || 0;
document.getElementById('cntParcial').textContent = data.contagens.parcial || 0;
document.getElementById('cntVencidos').textContent = data.contagens.vencidos || 0;
}
if (!data.data || data.data.length === 0) {
area.innerHTML = '<div class="empty-msg">🔍 Nenhum título encontrado</div>';
return;
}
renderCarnes(data, area);
})
.catch(function(err) {
if (err.message === 'redirect') return;
area.innerHTML = '<div class="empty-msg">⚠️ ' + err.message + '</div>';
});
}
function renderCarnes(data, area) {
var html = '<div style="overflow-x:auto"><table>' +
'<thead><tr>' +
'<th>#</th><th>Situação</th><th>Vencimento</th><th>Valor</th>' +
'<th>Pagamento</th><th>Agend. Cobrança</th><th>Nosso Número</th><th>Parcela</th>' +
'</tr></thead><tbody>';
for (var i = 0; i < data.data.length; i++) {
var c = data.data[i];
var bg = c.situacao.color || '#f3f4f6';
var tc = c.situacao.textColor || '#6b7280';
var vencDot = c.vencido ? '<span class="vencido-dot"></span>' : '';
html += '<tr>' +
'<td>' + c.id + '</td>' +
'<td><span class="sit-badge" style="background:' + bg + ';color:' + tc + '">' + c.situacao.descricao + '</span></td>' +
'<td>' + vencDot + fmtDate(c.vencimento) + '</td>' +
'<td class="valor">' + fmtMoney(c.valorParcela) + '</td>' +
'<td>' + fmtDate(c.dataPagamento) + '</td>' +
'<td style="font-size:11px">' + fmtDate(c.agendamentoCobranca) + '</td>' +
'<td>' + (c.nossoNumero || '-') + '</td>' +
'<td class="centro">' + (c.parcela || '-') + '/' + (c.totalParcelas || '-') + '</td>' +
'</tr>';
}
html += '</tbody></table></div>';
// Paginação
if (data.totalPages > 1) {
html += '<div class="pagination">';
html += '<button onclick="loadCarnes(' + (carnesPage - 1) + ')" ' + (carnesPage <= 1 ? 'disabled' : '') + '>← Anterior</button>';
var start = Math.max(1, carnesPage - 2);
var end = Math.min(data.totalPages, carnesPage + 2);
if (start > 1) {
html += '<button onclick="loadCarnes(1)">1</button>';
if (start > 2) html += '<button disabled>...</button>';
}
for (var p = start; p <= end; p++) {
html += '<button onclick="loadCarnes(' + p + ')" class="' + (p === carnesPage ? 'active' : '') + '">' + p + '</button>';
}
if (end < data.totalPages) {
if (end < data.totalPages - 1) html += '<button disabled>...</button>';
html += '<button onclick="loadCarnes(' + data.totalPages + ')">' + data.totalPages + '</button>';
}
html += '<button onclick="loadCarnes(' + (carnesPage + 1) + ')" ' + (carnesPage >= data.totalPages ? 'disabled' : '') + '>Próximo →</button>';
html += '</div>';
}
area.innerHTML = html;
}
// ===== EDITAR CONTATO =====
window.editarContato = function() {
var form = document.getElementById('editContatoForm');
document.getElementById('editEmail').value = document.getElementById('campoEmail').textContent === '-' ? '' : document.getElementById('campoEmail').textContent;
document.getElementById('editCelular').value = document.getElementById('campoCelular').textContent === '-' ? '' : document.getElementById('campoCelular').textContent;
document.getElementById('editTelefone').value = document.getElementById('campoTelefone').textContent === '-' ? '' : document.getElementById('campoTelefone').textContent;
form.style.display = 'block';
};
window.cancelarEditarContato = function() {
document.getElementById('editContatoForm').style.display = 'none';
};
window.salvarContato = async function() {
var email = document.getElementById('editEmail').value.trim();
var celular = document.getElementById('editCelular').value.trim();
var telefone = document.getElementById('editTelefone').value.trim();
var data = {};
if (email) data.email = email;
if (celular) data.celular = celular;
if (telefone) data.telefone = telefone;
if (Object.keys(data).length === 0) { alert('Preencha ao menos um campo'); return; }
try {
var r = await fetch('/api/' + alias + '/clients/' + clienteId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify(data)
});
var d = await r.json();
if (d.success) {
alert('Dados atualizados!');
document.getElementById('editContatoForm').style.display = 'none';
loadClient(); // Recarrega os dados
} else {
alert('Erro: ' + d.error);
}
} catch(e) { alert('Erro: ' + e.message); }
};
// ===== MODAL NOVA CONVERSA =====
window.abrirModalConversa = async function(empresaId, clienteId, nome, celular) {
document.getElementById('convEmpresaIdDet').value = empresaId;
document.getElementById('convClienteIdDet').value = clienteId;
document.getElementById('convNumeroDet').value = celular || '';
document.getElementById('convNomeDet').value = nome || '';
document.getElementById('convMensagemDet').value = 'Olá ' + (nome || '') + '! Tudo bem? Como podemos ajudar?';
var sel = document.getElementById('convInstanciaDet');
sel.innerHTML = '<option value="">Carregando...</option>';
try {
var r = await fetch('/api/' + alias + '/evolution/instances?empresaId=' + empresaId, {
headers: { 'Authorization': 'Bearer ' + token }
});
var d = await r.json();
if (d.success && d.data.length > 0) {
sel.innerHTML = d.data.map(function(i) {
return '<option value="' + i.id + '">' + i.nome + ' (' + i.instanceName + ')</option>';
}).join('');
} else {
sel.innerHTML = '<option value="">Nenhuma instância disponível</option>';
}
} catch(e) {
sel.innerHTML = '<option value="">Erro ao carregar</option>';
}
document.getElementById('modalNovaConvDet').style.display = 'flex';
};
window.iniciarConversaDet = async function() {
var empresaId = parseInt(document.getElementById('convEmpresaIdDet').value);
var numero = document.getElementById('convNumeroDet').value;
var nome = document.getElementById('convNomeDet').value;
var mensagem = document.getElementById('convMensagemDet').value.trim();
var instanciaId = document.getElementById('convInstanciaDet').value;
var clienteId = parseInt(document.getElementById('convClienteIdDet').value);
if (!numero || !mensagem) { alert('Informe o número e a mensagem'); return; }
var numCompleto = numero.startsWith('55') ? numero : '55' + numero;
try {
var r = await fetch('/api/' + alias + '/conversations/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ empresaId: empresaId, numero: numCompleto, nomeContato: nome, mensagem: mensagem, instanciaId: instanciaId ? parseInt(instanciaId) : null, clienteId: clienteId || null })
});
var d = await r.json();
if (d.success) {
document.getElementById('modalNovaConvDet').style.display = 'none';
window.location.href = '/app/' + alias + '/company/' + empresaId + '/conversation/' + d.data.id;
} else {
alert('Erro: ' + d.error);
}
} catch(e) { alert('Erro de conexão: ' + e.message); }
};
// Fechar modal ao clicar fora
document.getElementById('modalNovaConvDet').onclick = function(e) { if (e.target === this) this.style.display = 'none'; };
// ---- Iniciar ----
loadClient();
})();
</script>
<script src="/js/dark-mode.js"></script>
</body>
</html>
+346
View File
@@ -0,0 +1,346 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clientes - 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; }
</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">lajedo</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="#" class="active" id="navClients"><span class="icon">👥</span> Clientes</a>
<a href="#" 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">👥 Clientes</span>
<div class="user-info">
<span class="status-badge online">● Online</span>
<span class="user-name" id="userName"></span>
</div>
</div>
<div class="container">
<div class="search-bar">
<select id="empresaSelect">
<option value="">Todas as empresas</option>
</select>
<input type="text" id="searchInput" placeholder="Buscar por nome, matrícula ou CPF..." autofocus />
<button id="btnSearch">🔍 Buscar</button>
<span class="total-info" id="totalInfo"></span>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Matrícula</th>
<th>Nome</th>
<th>CPF</th>
<th>Telefone</th>
<th>Empresa</th>
<th>Situação</th>
<th>Ação</th>
</tr>
</thead>
<tbody id="tableBody">
<tr class="loading-row"><td colspan="7"><div class="spinner"></div><div style="margin-top:8px">Carregando...</div></td></tr>
</tbody>
</table>
<div class="pagination" id="pagination"></div>
</div>
</div>
</div>
<script>
const pathParts = window.location.pathname.split('/');
const 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') || '{}');
if (!token) { window.location.href = '/app/' + alias + '/login'; }
// Sidebar admin
const tipoChat = user.tipoChat || 'A';
if (tipoChat === 'G') {
document.querySelectorAll('.admin-only').forEach(function(el) { el.style.display = ''; });
}
document.getElementById('sidebarAlias').textContent = alias;
document.getElementById('userName').textContent = user.nome || '-';
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('navConfig').onclick = function(e) { navClick(e, '/app/' + alias + '/settings'); };
document.getElementById('navRoutes').onclick = function(e) { navClick(e, '/app/' + alias + '/routes'); };
function logout() {
['chatc2_token','chatc2_alias','chatc2_user'].forEach(k => localStorage.removeItem(k));
window.location.href = '/app/' + alias + '/login';
}
// === ESTADO ===
let currentPage = 1, currentSearch = '', currentEmpresa = '';
const LIMIT = 20;
const searchInput = document.getElementById('searchInput');
const btnSearch = document.getElementById('btnSearch');
const tableBody = document.getElementById('tableBody');
const paginationEl = document.getElementById('pagination');
const totalInfo = document.getElementById('totalInfo');
const empresaSelect = document.getElementById('empresaSelect');
// === CARREGAR EMPRESAS DO USUÁRIO ===
async function loadEmpresas() {
try {
const res = await fetch('/api/' + alias + '/empresas', {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await res.json();
if (data.success && data.data.length > 1) {
data.data.forEach(emp => {
const opt = document.createElement('option');
opt.value = emp.id;
opt.textContent = emp.nomeFantasia || emp.nome;
empresaSelect.appendChild(opt);
});
empresaSelect.style.display = '';
} else {
empresaSelect.style.display = 'none';
}
} catch (e) { /* ignora */ }
}
// === BUSCAR CLIENTES ===
async function fetchClients(page = 1, search = '', empresaId = '') {
tableBody.innerHTML = '<tr class=\"loading-row\"><td colspan=\"7\"><div class=\"spinner\"></div><div style=\"margin-top:8px\">Carregando...</div></td></tr>';
const params = new URLSearchParams({ q: search, page: page, limit: LIMIT });
if (empresaId) params.set('empresaId', empresaId);
try {
const res = await fetch('/api/' + alias + '/clients/search?' + params, {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await res.json();
if (!data.success) { showError(data.error); return; }
currentPage = data.page;
currentSearch = search;
currentEmpresa = empresaId;
renderTable(data.data, data.empresasPermitidas);
renderPagination(data.total, data.page, data.totalPages);
updateTotalInfo(data.total, data.page, data.totalPages);
} catch (err) {
showError('Erro de conexão: ' + err.message);
}
}
function renderTable(clients, empresasPermitidas) {
if (!clients || clients.length === 0) {
tableBody.innerHTML = '<tr><td colspan=\"7\"><div class=\"empty-state\"><div class=\"icon\">🔍</div><p>Nenhum cliente encontrado</p></div></td></tr>';
return;
}
// Mapeia empresaId -> nome
const empMap = {};
if (empresaSelect.options.length > 1) {
for (let i = 1; i < empresaSelect.options.length; i++) {
empMap[empresaSelect.options[i].value] = empresaSelect.options[i].textContent;
}
}
tableBody.innerHTML = clients.map(c => {
const sit = c.situacao === 'A' ? 'ativo' : 'inativo';
const sitLabel = c.situacao === 'A' ? 'Ativo' : 'Inativo';
const cel = c.celular || c.telefone || '-';
const empNome = empMap[c.empresaId] || 'Empresa ' + c.empresaId;
return '<tr>' +
'<td><strong>' + (c.matricula || '-') + '</strong></td>' +
'<td><a class="client-link" href="/app/' + alias + '/company/' + c.empresaId + '/client/' + c.id + '">' + c.nome + '</a></td>' +
'<td>' + (c.cpf || '-') + '</td>' +
'<td>' + cel + '</td>' +
'<td><span class="empresa-tag">' + empNome + '</span></td>' +
'<td><span class="situacao-badge ' + sit + '">' + sitLabel + '</span></td>' +
'<td>' +
'<a class="client-link" href="/app/' + alias + '/company/' + c.empresaId + '/client/' + c.id + '" title="Ver detalhes">Ver →</a>' +
' <a class="client-link" href="#" onclick="abrirModalConversa(' + c.empresaId + ',' + c.id + ',\'' + (c.nome||'').replace(/'/g,"\\'") + '\',\'' + (c.celular||'').replace(/\D/g,'') + '\');return false" title="Iniciar conversa">💬</a>' +
'</tr>';
}).join('');
}
function renderPagination(total, page, totalPages) {
if (!total || total === 0) { paginationEl.innerHTML = ''; return; }
let html = '<button onclick="goToPage(' + (page - 1) + ')" ' + (page <= 1 ? 'disabled' : '') + '>← Anterior</button>';
const start = Math.max(1, page - 2);
const end = Math.min(totalPages, page + 2);
if (start > 1) {
html += '<button onclick="goToPage(1)">1</button>';
if (start > 2) html += '<button disabled>...</button>';
}
for (let i = start; i <= end; i++) {
html += '<button onclick="goToPage(' + i + ')" class="' + (i === page ? 'active' : '') + '">' + i + '</button>';
}
if (end < totalPages) {
if (end < totalPages - 1) html += '<button disabled>...</button>';
html += '<button onclick="goToPage(' + totalPages + ')">' + totalPages + '</button>';
}
html += '<button onclick="goToPage(' + (page + 1) + ')" ' + (page >= totalPages ? 'disabled' : '') + '>Próximo →</button>';
paginationEl.innerHTML = html;
}
function updateTotalInfo(total, page, totalPages) {
if (total === 0) { totalInfo.textContent = ''; return; }
const start = ((page - 1) * LIMIT) + 1;
const end = Math.min(page * LIMIT, total);
totalInfo.textContent = start + '-' + end + ' de ' + total + ' (pág ' + page + '/' + totalPages + ')';
}
function goToPage(page) { if (page < 1) return; fetchClients(page, currentSearch, currentEmpresa); }
function showError(msg) {
tableBody.innerHTML = '<tr><td colspan=\"7\"><div class=\"empty-state\"><div class=\"icon\">⚠️</div><p>' + msg + '</p></div></td></tr>';
paginationEl.innerHTML = '';
totalInfo.textContent = '';
}
// === EVENTOS ===
btnSearch.addEventListener('click', () => fetchClients(1, searchInput.value.trim(), empresaSelect.value));
searchInput.addEventListener('keydown', e => { if (e.key === 'Enter') fetchClients(1, searchInput.value.trim(), empresaSelect.value); });
empresaSelect.addEventListener('change', () => fetchClients(1, searchInput.value.trim(), empresaSelect.value));
// === INICIAR ===
loadEmpresas().then(() => fetchClients(1, '', ''));
</script>
<script src="/js/dark-mode.js"></script>
</body>
</html>
<!-- Modal Nova Conversa -->
<div class="modal-overlay" id="modalNovaConv" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:450px">
<h3 style="margin-bottom:16px">💬 Iniciar Conversa</h3>
<input type="hidden" id="convClienteId">
<input type="hidden" id="convEmpresaId">
<div class="form-group"><label>Número</label><input type="text" id="convNumero" readonly style="background:#f9fafb"></div>
<div class="form-group"><label>Cliente</label><input type="text" id="convNome" readonly style="background:#f9fafb"></div>
<div class="form-group"><label>Instância WhatsApp</label><select id="convInstancia" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px"></select></div>
<div class="form-group"><label>Mensagem inicial</label><textarea id="convMensagem" rows="3" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px;resize:vertical" placeholder="Digite a mensagem que será enviada..."></textarea></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn" onclick="fecharModalConv()" style="padding:8px 16px;border:1px solid #d1d5db;border-radius:8px;background:#fff;cursor:pointer">Cancelar</button>
<button class="btn btn-primary" onclick="iniciarConversa()" style="padding:8px 16px;background:#667eea;color:#fff;border:none;border-radius:8px;cursor:pointer">💬 Enviar e Abrir Chat</button>
</div>
</div>
</div>
<script>
// ===== NOVA CONVERSA =====
var modalConvEl = document.getElementById('modalNovaConv');
window.abrirModalConversa = async function(empresaId, clienteId, nome, celular) {
document.getElementById('convEmpresaId').value = empresaId;
document.getElementById('convClienteId').value = clienteId;
document.getElementById('convNumero').value = celular || '';
document.getElementById('convNome').value = nome || '';
document.getElementById('convMensagem').value = 'Olá ' + (nome || '') + '! Tudo bem? Como podemos ajudar?';
// Carregar instâncias
var sel = document.getElementById('convInstancia');
sel.innerHTML = '<option value="">Carregando...</option>';
try {
var r = await fetch('/api/' + alias + '/evolution/instances?empresaId=' + empresaId, {
headers: { 'Authorization': 'Bearer ' + token }
});
var d = await r.json();
if (d.success && d.data.length > 0) {
sel.innerHTML = d.data.map(function(i) {
return '<option value="' + i.id + '">' + i.nome + ' (' + i.instanceName + ')</option>';
}).join('');
} else {
sel.innerHTML = '<option value="">Nenhuma instância disponível</option>';
}
} catch(e) {
sel.innerHTML = '<option value="">Erro ao carregar</option>';
}
modalConvEl.style.display = 'flex';
};
window.fecharModalConv = function() {
modalConvEl.style.display = 'none';
};
window.iniciarConversa = async function() {
var empresaId = document.getElementById('convEmpresaId').value;
var numero = document.getElementById('convNumero').value;
var nome = document.getElementById('convNome').value;
var mensagem = document.getElementById('convMensagem').value.trim();
var instanciaId = document.getElementById('convInstancia').value;
var clienteId = document.getElementById('convClienteId').value;
if (!numero || !mensagem) {
alert('Informe o número e a mensagem');
return;
}
// Adicionar código do país se necessário
var numCompleto = numero.startsWith('55') ? numero : '55' + numero;
try {
var r = await fetch('/api/' + alias + '/conversations/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ empresaId: parseInt(empresaId), numero: numCompleto, nomeContato: nome, mensagem: mensagem, instanciaId: instanciaId ? parseInt(instanciaId) : null, clienteId: clienteId ? parseInt(clienteId) : null })
});
var d = await r.json();
if (d.success) {
fecharModalConv();
window.location.href = '/app/' + alias + '/company/' + empresaId + '/conversation/' + d.data.id;
} else {
alert('Erro: ' + d.error);
}
} catch(e) {
alert('Erro de conexão: ' + e.message);
}
};
// Fechar modal ao clicar fora
modalConvEl.addEventListener('click', function(e) { if (e.target === this) this.style.display = 'none'; });
</script>
+208
View File
@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Avalie seu Atendimento</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: #fff;
border-radius: 20px;
padding: 40px;
max-width: 480px;
width: 100%;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
}
.logo { font-size: 48px; margin-bottom: 16px; }
h1 { font-size: 24px; color: #1f2937; margin-bottom: 8px; }
.subtitle { font-size: 14px; color: #6b7280; margin-bottom: 32px; }
.stars {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 32px;
direction: rtl;
}
.stars input { display: none; }
.stars label {
font-size: 48px;
cursor: pointer;
color: #d1d5db;
transition: color .2s, transform .15s;
}
.stars label:hover,
.stars label:hover ~ label,
.stars input:checked ~ label {
color: #f59e0b;
transform: scale(1.1);
}
.stars input:checked + label {
color: #f59e0b;
}
textarea {
width: 100%;
padding: 14px 16px;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 80px;
outline: none;
transition: border-color .2s;
margin-bottom: 20px;
}
textarea:focus { border-color: #667eea; }
.btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: opacity .2s;
}
.btn:hover { opacity: .9; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.rating-text {
font-size: 14px;
color: #6b7280;
margin-bottom: 24px;
min-height: 20px;
}
.success { display: none; }
.success .icon { font-size: 64px; margin-bottom: 16px; }
.success h2 { color: #059669; margin-bottom: 8px; }
.success p { color: #6b7280; }
.erro {
color: #ef4444;
font-size: 14px;
margin-top: 12px;
display: none;
}
</style>
</head>
<body>
<div class="container" id="app">
<div class="logo">💬</div>
<h1>Avalie seu Atendimento</h1>
<p class="subtitle">Sua opinião é muito importante para melhorarmos nosso serviço</p>
<div class="rating-text" id="ratingText">Toque nas estrelas para avaliar</div>
<div class="stars" id="starContainer">
<input type="radio" name="star" id="star5" value="5">
<label for="star5" title="Excelente"></label>
<input type="radio" name="star" id="star4" value="4">
<label for="star4" title="Bom"></label>
<input type="radio" name="star" id="star3" value="3">
<label for="star3" title="Regular"></label>
<input type="radio" name="star" id="star2" value="2">
<label for="star2" title="Ruim"></label>
<input type="radio" name="star" id="star1" value="1">
<label for="star1" title="Péssimo"></label>
</div>
<textarea id="comentario" placeholder="Deixe seu comentário (opcional)..."></textarea>
<button class="btn" id="btnEnviar" onclick="enviar()">Enviar Avaliação</button>
<div class="erro" id="erro"></div>
<div class="success" id="success">
<div class="icon"></div>
<h2>Agradecemos sua avaliação!</h2>
<p>Seu feedback nos ajuda a melhorar cada vez mais.</p>
</div>
</div>
<script>
var alias, conversaId, empresaId;
function getParams() {
var params = new URLSearchParams(window.location.search);
alias = params.get('alias');
conversaId = params.get('conversa');
empresaId = params.get('empresa');
if (!alias || !conversaId || !empresaId) {
document.getElementById('erro').textContent = 'Link inválido. Entre em contato conosco.';
document.getElementById('erro').style.display = 'block';
document.querySelector('.stars').style.display = 'none';
document.querySelector('textarea').style.display = 'none';
document.getElementById('btnEnviar').style.display = 'none';
}
}
var nota = 0;
document.querySelectorAll('.stars input').forEach(function(input) {
input.addEventListener('change', function() {
nota = parseInt(this.value);
var textos = ['', 'Péssimo', 'Ruim', 'Regular', 'Bom', 'Excelente!'];
document.getElementById('ratingText').textContent = textos[nota] || '';
});
});
async function enviar() {
if (nota === 0) {
document.getElementById('erro').textContent = 'Selecione uma avaliação de 1 a 5 estrelas.';
document.getElementById('erro').style.display = 'block';
return;
}
var btn = document.getElementById('btnEnviar');
btn.disabled = true;
btn.textContent = 'Enviando...';
document.getElementById('erro').style.display = 'none';
try {
var resp = await fetch('/api/' + alias + '/csat/avaliar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversaId: parseInt(conversaId),
empresaId: parseInt(empresaId),
nota: nota,
comentario: document.getElementById('comentario').value.trim()
})
});
var data = await resp.json();
if (data.success) {
document.getElementById('app').querySelector('.stars').style.display = 'none';
document.querySelector('textarea').style.display = 'none';
document.getElementById('btnEnviar').style.display = 'none';
document.getElementById('ratingText').style.display = 'none';
document.getElementById('success').style.display = 'block';
} else {
throw new Error(data.error || 'Erro ao enviar');
}
} catch(e) {
document.getElementById('erro').textContent = 'Erro ao enviar: ' + e.message;
document.getElementById('erro').style.display = 'block';
btn.disabled = false;
btn.textContent = 'Enviar Avaliação';
}
}
getParams();
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+648
View File
@@ -0,0 +1,648 @@
/* =====================================================
CHATC2 - Design System Compartilhado
===================================================== */
/* ===== VARIÁVEIS ===== */
:root {
--primary: #667eea;
--primary-dark: #5a67d8;
--secondary: #764ba2;
--sidebar-from: #1e1b4b;
--sidebar-to: #312e81;
--surface: #ffffff;
--surface-2: #f9fafb;
--surface-3: #f3f4f6;
--border: #e5e7eb;
--border-light: #f3f4f6;
--text-primary: #111827;
--text-secondary: #374151;
--text-muted: #6b7280;
--text-faint: #9ca3af;
--success: #059669;
--success-bg: #d1fae5;
--success-text: #065f46;
--danger: #ef4444;
--danger-bg: #fef2f2;
--danger-text: #991b1b;
--warning: #f59e0b;
--warning-bg: #fef3c7;
--warning-text: #92400e;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-md: 0 4px 16px rgba(0,0,0,0.10);
--shadow-lg: 0 20px 60px rgba(0,0,0,0.18);
--transition: all 0.15s ease;
}
/* ===== RESET GLOBAL ===== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
color: var(--text-primary);
}
/* ===== SIDEBAR COMPARTILHADA ===== */
.sidebar {
width: 240px;
background: linear-gradient(180deg, var(--sidebar-from) 0%, var(--sidebar-to) 100%);
color: #fff;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-brand {
padding: 20px 18px;
border-bottom: 1px solid rgba(255,255,255,0.08);
display: flex;
align-items: center;
gap: 12px;
}
.sidebar-brand .logo {
width: 38px;
height: 38px;
background: rgba(255,255,255,0.18);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 15px;
flex-shrink: 0;
overflow: hidden;
}
.sidebar-brand .logo img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 10px;
}
.sidebar-brand h2 {
font-size: 16px;
font-weight: 700;
color: #fff;
letter-spacing: -0.2px;
}
.sidebar-brand span {
font-size: 11px;
color: rgba(255,255,255,0.45);
display: block;
margin-top: 1px;
}
.sidebar-nav {
padding: 12px 10px;
flex: 1;
overflow-y: auto;
}
.sidebar-nav::-webkit-scrollbar { width: 3px; }
.sidebar-nav::-webkit-scrollbar-track { background: transparent; }
.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
.sidebar-nav .nav-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(255,255,255,0.35);
padding: 10px 10px 4px;
font-weight: 600;
}
.sidebar-nav a {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border-radius: 8px;
color: rgba(255,255,255,0.72);
text-decoration: none;
font-size: 14px;
transition: var(--transition);
margin-bottom: 1px;
}
.sidebar-nav a:hover {
background: rgba(255,255,255,0.10);
color: #fff;
}
.sidebar-nav a.active {
background: rgba(255,255,255,0.16);
color: #fff;
font-weight: 600;
}
.sidebar-nav a .icon {
width: 22px;
text-align: center;
font-size: 16px;
flex-shrink: 0;
}
.sidebar-footer {
padding: 12px 10px;
border-top: 1px solid rgba(255,255,255,0.08);
}
.sidebar-footer .dark-mode-toggle {
width: 100%;
margin-bottom: 4px;
padding: 9px 12px;
border-radius: 8px;
cursor: pointer;
background: rgba(255,255,255,0.07);
border: 1px solid rgba(255,255,255,0.12);
font-size: 13px;
color: rgba(255,255,255,0.75);
transition: var(--transition);
text-align: left;
display: flex;
align-items: center;
gap: 8px;
}
.sidebar-footer .dark-mode-toggle:hover {
background: rgba(255,255,255,0.14);
color: #fff;
}
.sidebar-footer a {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border-radius: 8px;
color: rgba(255,255,255,0.55);
text-decoration: none;
font-size: 13px;
transition: var(--transition);
cursor: pointer;
}
.sidebar-footer a:hover {
background: rgba(255,255,255,0.10);
color: #fca5a5;
}
/* ===== TOPBAR ===== */
.topbar {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 14px 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--shadow-sm);
flex-shrink: 0;
}
.topbar-title {
font-size: 17px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.2px;
}
.user-info {
display: flex;
align-items: center;
gap: 14px;
}
.user-name {
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.status-badge.online {
background: var(--success-bg);
color: var(--success-text);
}
/* ===== CARDS ===== */
.card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: 24px;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-light);
}
.card h3 {
font-size: 15px;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 16px;
}
/* ===== INFO GRID ===== */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.info-item {
padding: 14px 16px;
background: var(--surface-2);
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
}
.info-item .label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--text-faint);
margin-bottom: 4px;
font-weight: 600;
}
.info-item .value {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
}
/* ===== BOTÕES ===== */
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 9px 18px;
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
transition: var(--transition);
text-decoration: none;
white-space: nowrap;
}
.btn-primary {
background: var(--primary);
color: #fff;
border-color: var(--primary);
}
.btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }
.btn-secondary {
background: var(--surface);
color: var(--text-secondary);
border-color: var(--border);
}
.btn-secondary:hover { background: var(--surface-2); }
.btn-danger {
background: var(--danger);
color: #fff;
border-color: var(--danger);
}
.btn-danger:hover { background: #dc2626; }
/* ===== FORMULÁRIOS ===== */
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 6px;
}
.form-control,
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
select,
textarea {
width: 100%;
padding: 10px 14px;
border: 2px solid var(--border);
border-radius: var(--radius-md);
font-size: 14px;
font-family: inherit;
color: var(--text-primary);
background: var(--surface-2);
outline: none;
transition: var(--transition);
}
.form-control:focus,
input[type="text"]:focus,
input[type="password"]:focus,
select:focus,
textarea:focus {
border-color: var(--primary);
background: var(--surface);
box-shadow: 0 0 0 3px rgba(102,126,234,0.12);
}
/* ===== TABELAS ===== */
.table-wrapper {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-light);
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background: var(--surface-2);
}
th {
text-align: left;
padding: 12px 16px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
font-weight: 700;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
td {
padding: 12px 16px;
font-size: 14px;
border-bottom: 1px solid var(--border-light);
color: var(--text-secondary);
}
tr:hover td { background: var(--surface-2); }
tr:last-child td { border-bottom: none; }
/* ===== BADGES ===== */
.badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.situacao-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
.situacao-badge.ativo { background: var(--success-bg); color: var(--success-text); }
.situacao-badge.inativo { background: var(--danger-bg); color: var(--danger-text); }
.empresa-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 5px;
font-size: 11px;
font-weight: 600;
background: #ede9fe;
color: #5b21b6;
}
.client-link {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.client-link:hover { text-decoration: underline; }
/* ===== PAGINAÇÃO ===== */
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
padding: 14px 16px;
border-top: 1px solid var(--border-light);
}
.pagination button {
padding: 6px 12px;
border: 1px solid var(--border);
background: var(--surface);
border-radius: var(--radius-sm);
font-size: 13px;
cursor: pointer;
transition: var(--transition);
color: var(--text-secondary);
font-weight: 500;
}
.pagination button:hover:not(:disabled) { background: var(--surface-2); border-color: var(--text-faint); }
.pagination button.active { background: var(--primary); border-color: var(--primary); color: #fff; }
.pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
.pagination .page-info { font-size: 13px; color: var(--text-muted); padding: 0 10px; }
/* ===== MODAL ===== */
.modal-overlay {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.48);
backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay.show { display: flex; }
.modal-box {
background: var(--surface);
border-radius: var(--radius-lg);
padding: 28px;
width: 90%;
max-width: 440px;
box-shadow: var(--shadow-lg);
animation: modalIn 0.18s ease;
}
@keyframes modalIn {
from { opacity: 0; transform: scale(0.96) translateY(-10px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.modal-box h3 {
margin-bottom: 18px;
font-size: 17px;
font-weight: 700;
color: var(--text-primary);
}
.modal-footer {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 20px;
}
/* ===== ESTADOS VAZIOS ===== */
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-faint);
}
.empty-state .icon { font-size: 48px; margin-bottom: 12px; display: block; }
.empty-state p { font-size: 14px; }
/* ===== SPINNERS ===== */
.spinner {
display: inline-block;
width: 22px; height: 22px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-row td {
text-align: center;
padding: 40px;
color: var(--text-faint);
}
/* ===== TOKEN BOX ===== */
.token-box {
background: #1f2937;
color: #e5e7eb;
padding: 16px;
border-radius: var(--radius-md);
font-family: 'SF Mono', 'Cascadia Code', Monaco, monospace;
font-size: 12px;
word-break: break-all;
line-height: 1.6;
max-height: 120px;
overflow-y: auto;
}
.btn-copy {
margin-top: 12px;
padding: 8px 16px;
background: #374151;
color: #fff;
border: none;
border-radius: var(--radius-sm);
font-size: 13px;
cursor: pointer;
transition: var(--transition);
font-weight: 500;
}
.btn-copy:hover { background: #4b5563; }
/* ===== SEARCH BAR ===== */
.search-bar {
display: flex;
gap: 10px;
margin-bottom: 20px;
align-items: center;
flex-wrap: wrap;
}
.search-bar input {
flex: 1;
min-width: 180px;
padding: 10px 16px;
border: 2px solid var(--border);
border-radius: var(--radius-md);
font-size: 14px;
outline: none;
background: var(--surface);
transition: var(--transition);
}
.search-bar input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(102,126,234,0.1); }
.search-bar select {
padding: 10px 16px;
border: 2px solid var(--border);
border-radius: var(--radius-md);
font-size: 14px;
outline: none;
background: var(--surface);
min-width: 200px;
cursor: pointer;
transition: var(--transition);
color: var(--text-primary);
}
.search-bar select:focus { border-color: var(--primary); }
.search-bar button {
padding: 10px 20px;
background: var(--primary);
color: #fff;
border: none;
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
}
.search-bar button:hover { background: var(--primary-dark); }
.search-bar button:disabled { opacity: 0.5; cursor: not-allowed; }
.search-bar .total-info {
font-size: 13px;
color: var(--text-muted);
margin-left: auto;
white-space: nowrap;
}
/* ===== BACK BUTTON ===== */
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--primary);
text-decoration: none;
font-size: 14px;
font-weight: 500;
margin-bottom: 16px;
transition: var(--transition);
}
.back-btn:hover { color: var(--primary-dark); }
/* ===== SCROLLBAR CUSTOMIZADA ===== */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #9ca3af; }
+188
View File
@@ -0,0 +1,188 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Chatc2</title>
<link rel="stylesheet" href="/css/main.css">
<style>
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; }
</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>
<!-- SIDEBAR -->
<aside class="sidebar">
<div class="sidebar-brand">
<div class="logo">C2</div>
<div>
<h2>Chatc2</h2>
<span id="sidebarAlias">lajedo</span>
</div>
</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>
<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="navAllConvs" style="display:none"><span class="icon">💬</span> Todas Conversas</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>
</div>
</aside>
<!-- MAIN -->
<div class="main">
<div class="topbar">
<span class="topbar-title">Dashboard</span>
<div class="user-info">
<span class="status-badge online">● Online</span>
<span class="user-name" id="userName"></span>
</div>
</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>
<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>
</div>
</div>
<script>
// Extrai alias da URL
const pathParts = window.location.pathname.split('/');
const 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') || '{}');
if (!token) {
window.location.href = '/app/' + alias + '/login';
}
// Sidebar - mostra opções de admin se for Gerente
const tipoChat = user.tipoChat || 'A';
if (tipoChat === 'G') {
document.getElementById('adminLabel').style.display = '';
document.getElementById('navConfig').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';
}
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;
}
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('navConfig').onclick = function(e) { navClick(e, '/app/' + alias + '/settings'); };
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');
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';
}
}
</script>
<script src="/js/dark-mode.js"></script>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
// Dark Mode Controller - Only handles initialization/persistence
(function() {
if (typeof window.darkModeToggle !== 'function') {
// Ensure function exists (inline script in head should have defined it)
window.darkModeToggle = function() {
var el = document.body;
if (!el) return;
var isDark = localStorage.getItem('chatc2_dark_mode') === 'true';
var enable = !isDark;
el.classList.toggle('dark-mode', enable);
localStorage.setItem('chatc2_dark_mode', enable ? 'true' : 'false');
document.querySelectorAll('.dark-mode-toggle').forEach(function(btn) {
btn.innerHTML = enable ? '☀️ Claro' : '🌙 Escuro';
});
};
window.darkModeApply = function(enable) {
var el = document.body;
if (!el) return;
el.classList.toggle('dark-mode', enable);
localStorage.setItem('chatc2_dark_mode', enable ? 'true' : 'false');
document.querySelectorAll('.dark-mode-toggle').forEach(function(btn) {
btn.innerHTML = enable ? '☀️ Claro' : '🌙 Escuro';
});
};
window.darkModeIsDark = function() { return localStorage.getItem('chatc2_dark_mode') === 'true'; };
}
// Apply on load
if (localStorage.getItem('chatc2_dark_mode') === 'true') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
document.body.classList.add('dark-mode');
document.querySelectorAll('.dark-mode-toggle').forEach(function(btn) {
btn.innerHTML = '☀️ Claro';
});
});
} else {
document.body.classList.add('dark-mode');
document.querySelectorAll('.dark-mode-toggle').forEach(function(btn) {
btn.innerHTML = '☀️ Claro';
});
}
}
})();
+359
View File
@@ -0,0 +1,359 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Chatc2</title>
<link rel="stylesheet" href="/css/main.css">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-size: 400% 400%;
animation: gradientShift 12s ease infinite;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.login-container {
background: #fff;
border-radius: 20px;
padding: 44px 40px 36px;
width: 100%;
max-width: 400px;
box-shadow: 0 32px 80px rgba(0,0,0,0.28), 0 0 0 1px rgba(255,255,255,0.1);
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-header .logo {
width: 76px;
height: 76px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 18px;
font-size: 30px;
color: #fff;
font-weight: 800;
overflow: hidden;
box-shadow: 0 8px 24px rgba(102,126,234,0.4);
}
.login-header .logo img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 20px;
}
.login-header h1 {
font-size: 24px;
font-weight: 800;
color: #111827;
margin-bottom: 6px;
letter-spacing: -0.5px;
}
.login-header p {
color: #6b7280;
font-size: 14px;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 7px;
}
.form-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 11px;
font-size: 15px;
transition: all 0.15s;
outline: none;
background: #f9fafb;
color: #111827;
}
.form-group input:focus {
border-color: #667eea;
background: #fff;
box-shadow: 0 0 0 4px rgba(102,126,234,0.12);
}
.form-group input.error {
border-color: #ef4444;
background: #fef2f2;
}
.password-wrapper { position: relative; }
.password-wrapper input { padding-right: 46px; }
.toggle-password {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: #9ca3af;
font-size: 18px;
padding: 4px;
transition: color 0.15s;
}
.toggle-password:hover { color: #6b7280; }
.btn-login {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
border: none;
border-radius: 11px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
margin-top: 4px;
letter-spacing: 0.2px;
}
.btn-login:hover { opacity: 0.92; }
.btn-login:active { transform: scale(0.98); }
.btn-login:disabled { opacity: 0.6; cursor: not-allowed; }
.error-message {
background: #fef2f2;
color: #dc2626;
padding: 12px 16px;
border-radius: 10px;
font-size: 13px;
margin-bottom: 16px;
display: none;
border: 1px solid #fecaca;
font-weight: 500;
}
.error-message.show { display: block; }
.loading-spinner {
display: none;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,0.35);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin: 0 auto;
}
.btn-login.loading .btn-text { display: none; }
.btn-login.loading .loading-spinner { display: block; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Dark mode toggle no login */
.login-dark-toggle {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 9999;
}
.login-dark-toggle button {
padding: 10px 16px;
border-radius: 24px;
border: 1px solid rgba(255,255,255,0.3);
background: rgba(255,255,255,0.15);
backdrop-filter: blur(8px);
color: #fff;
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
font-weight: 500;
}
.login-dark-toggle button:hover {
background: rgba(255,255,255,0.25);
}
</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>
<div class="login-container">
<div class="login-header">
<div class="logo" id="logoEl">C2</div>
<h1>Bem-vindo</h1>
<p id="empresaNome">Faça login para acessar o Chatc2</p>
</div>
<div id="errorMessage" class="error-message"></div>
<form id="loginForm" autocomplete="off">
<div class="form-group">
<label for="USU_LOGIN">Usuário</label>
<input
type="text"
id="USU_LOGIN"
name="USU_LOGIN"
placeholder="Digite seu usuário"
required
autofocus
/>
</div>
<div class="form-group">
<label for="USU_SENHA">Senha</label>
<div class="password-wrapper">
<input
type="password"
id="USU_SENHA"
name="USU_SENHA"
placeholder="Digite sua senha"
required
/>
<button type="button" class="toggle-password" id="togglePassword" tabindex="-1">
👁️
</button>
</div>
</div>
<button type="submit" class="btn-login" id="btnLogin">
<span class="btn-text">Entrar</span>
<div class="loading-spinner"></div>
</button>
</form>
</div>
<script>
// Extrai o alias da URL: /app/:alias/login
const pathParts = window.location.pathname.split('/');
const alias = pathParts[2] || 'lajedo';
const form = document.getElementById('loginForm');
const btnLogin = document.getElementById('btnLogin');
const errorMsg = document.getElementById('errorMessage');
const loginInput = document.getElementById('USU_LOGIN');
const senhaInput = document.getElementById('USU_SENHA');
const togglePassword = document.getElementById('togglePassword');
const logoEl = document.getElementById('logoEl');
const empresaNome = document.getElementById('empresaNome');
// Mostrar/ocultar senha
togglePassword.addEventListener('click', () => {
const type = senhaInput.getAttribute('type') === 'password' ? 'text' : 'password';
senhaInput.setAttribute('type', type);
togglePassword.textContent = type === 'password' ? '👁️' : '👁️‍🗨️';
});
// Limpar erro ao digitar
loginInput.addEventListener('input', () => {
errorMsg.classList.remove('show');
loginInput.classList.remove('error');
senhaInput.classList.remove('error');
});
senhaInput.addEventListener('input', () => {
errorMsg.classList.remove('show');
senhaInput.classList.remove('error');
});
// Carrega logo da empresa
async function carregarLogo() {
try {
const res = await fetch('/api/' + alias + '/empresa/logo');
const data = await res.json();
if (data.success) {
if (data.fotoUrl) {
logoEl.innerHTML = '<img src="' + data.fotoUrl + '" alt="Logo">';
} else if (data.iniciais) {
logoEl.textContent = data.iniciais;
logoEl.style.background = data.cor || 'linear-gradient(135deg, #667eea, #764ba2)';
}
if (data.nomeFantasia) {
empresaNome.textContent = data.nomeFantasia;
}
}
} catch(e) { /* usa padrão */ }
}
carregarLogo();
// Submit do formulário
form.addEventListener('submit', async (e) => {
e.preventDefault();
const USU_LOGIN = loginInput.value.trim().toUpperCase();
const USU_SENHA = senhaInput.value;
if (!USU_LOGIN || !USU_SENHA) {
showError('Preencha todos os campos.');
return;
}
btnLogin.classList.add('loading');
btnLogin.disabled = true;
errorMsg.classList.remove('show');
try {
const response = await fetch('/app/' + alias + '/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ USU_LOGIN, USU_SENHA }),
});
const data = await response.json();
if (!response.ok || !data.success) {
showError(data.error || 'Erro ao fazer login.');
return;
}
// Salva o token, dados do usuário e alias
localStorage.setItem('chatc2_token', data.token);
localStorage.setItem('chatc2_alias', data.alias || alias);
localStorage.setItem('chatc2_user', JSON.stringify(data.user));
// Redireciona para o dashboard com o alias
window.location.href = '/app/' + alias + '/dashboard';
} catch (err) {
showError('Erro de conexão com o servidor.');
} finally {
btnLogin.classList.remove('loading');
btnLogin.disabled = false;
}
});
function showError(message) {
errorMsg.textContent = message;
errorMsg.classList.add('show');
loginInput.classList.add('error');
senhaInput.classList.add('error');
}
</script>
<div class="login-dark-toggle">
<button class="dark-mode-toggle" onclick="darkModeToggle()">🌙 Escuro</button>
</div>
<script src="/js/dark-mode.js"></script>
</body>
</html>
+336
View File
@@ -0,0 +1,336 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rotas - Chatc2 API</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; }
/* Barra de busca/filtros */
.search-box { margin-bottom:18px; display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
.search-box input { flex:1; min-width:220px; padding:10px 16px; border:2px solid #e5e7eb; border-radius:8px; font-size:14px; outline:none; transition:border-color .15s; background:#fff; color:#1f2937; }
.search-box input:focus { border-color:#667eea; }
.search-box select { padding:10px 12px; border:2px solid #e5e7eb; border-radius:8px; font-size:13px; outline:none; background:#fff; cursor:pointer; color:#374151; }
.search-box select:focus { border-color:#667eea; }
.btn-ghost { padding:9px 14px; border:2px solid #e5e7eb; border-radius:8px; background:#fff; cursor:pointer; font-size:13px; color:#374151; }
.btn-ghost:hover { background:#f3f4f6; }
/* Legenda */
.legenda { display:flex; gap:16px; flex-wrap:wrap; font-size:12px; color:#6b7280; margin-bottom:16px; align-items:center; }
.legenda .pill { display:inline-flex; align-items:center; gap:5px; }
/* Categoria */
.cat-group { margin-bottom:26px; }
.cat-title { display:flex; align-items:center; gap:8px; font-size:15px; font-weight:700; color:#374151; margin:0 0 10px; }
.cat-title .count { font-size:11px; background:#e5e7eb; color:#6b7280; padding:2px 8px; border-radius:10px; font-weight:600; }
/* Card de rota (expansível) */
.route { background:#fff; border:1px solid #e5e7eb; border-radius:10px; margin-bottom:8px; overflow:hidden; }
.route[open] { box-shadow:0 1px 6px rgba(0,0,0,.06); border-color:#d1d5db; }
.route-head { list-style:none; cursor:pointer; display:flex; align-items:center; gap:10px; padding:12px 14px; flex-wrap:wrap; }
.route-head::-webkit-details-marker { display:none; }
.route-head:hover { background:#fafafa; }
.route[open] .route-head { border-bottom:1px solid #f0f0f0; background:#fafbff; }
.chev { color:#9ca3af; font-size:11px; transition:transform .15s; }
.route[open] .chev { transform:rotate(90deg); }
.method { display:inline-block; padding:3px 9px; border-radius:5px; font-size:11px; font-weight:700; text-transform:uppercase; color:#fff; min-width:54px; text-align:center; }
.method.get { background:#059669; }
.method.post { background:#2563eb; }
.method.put { background:#d97706; }
.method.delete { background:#dc2626; }
.method.patch { background:#7c3aed; }
.route-path { font-family:'SF Mono',Monaco,'Cascadia Code',monospace; font-size:13px; color:#1f2937; word-break:break-all; }
.route-path .ph { color:#7c3aed; font-style:normal; }
.route-summary-desc { color:#6b7280; font-size:12.5px; flex:1 1 100%; margin-left:64px; }
.auth-badge { font-size:10.5px; font-weight:600; padding:2px 8px; border-radius:20px; white-space:nowrap; }
.auth-yes { background:#fef3c7; color:#92400e; }
.auth-no { background:#d1fae5; color:#065f46; }
/* Detalhe expandido */
.route-detail { padding:14px 16px 16px; }
.block { margin-top:14px; }
.block:first-child { margin-top:0; }
.block-title { font-size:12px; font-weight:700; color:#6b7280; text-transform:uppercase; letter-spacing:.4px; margin-bottom:7px; display:flex; align-items:center; gap:8px; }
.param-table { width:100%; border-collapse:collapse; font-size:13px; }
.param-table th { text-align:left; font-size:10.5px; text-transform:uppercase; color:#9ca3af; padding:4px 10px; border-bottom:1px solid #eee; font-weight:600; }
.param-table td { padding:6px 10px; border-bottom:1px solid #f3f4f6; vertical-align:top; color:#374151; }
.param-table td.pname { font-family:'SF Mono',monospace; color:#1f2937; font-weight:600; white-space:nowrap; }
.tag { font-size:10px; font-weight:700; padding:2px 7px; border-radius:12px; white-space:nowrap; }
.tag.req { background:#fee2e2; color:#b91c1c; }
.tag.opt { background:#f3f4f6; color:#6b7280; }
.code-json { background:#0f172a; color:#e2e8f0; border-radius:8px; padding:12px 14px; font-family:'SF Mono',Monaco,'Cascadia Code',monospace; font-size:12.5px; line-height:1.55; overflow-x:auto; margin:0; white-space:pre; }
.code-curl { background:#111827; color:#a7f3d0; border-radius:8px; padding:12px 14px; font-family:'SF Mono',monospace; font-size:12px; line-height:1.5; overflow-x:auto; margin:0; white-space:pre; }
.req-note { font-size:12px; color:#6b7280; margin-top:7px; }
.req-note code { background:#f3f4f6; padding:1px 6px; border-radius:4px; color:#b91c1c; font-weight:600; }
.note-box { font-size:12.5px; color:#475569; background:#f8fafc; border-left:3px solid #cbd5e1; padding:8px 12px; border-radius:0 6px 6px 0; }
.copy { padding:3px 9px; border:none; border-radius:5px; background:#e5e7eb; cursor:pointer; font-size:11px; color:#374151; }
.copy:hover { background:#d1d5db; }
.copy-light { background:rgba(255,255,255,.12); color:#e2e8f0; }
.copy-light:hover { background:rgba(255,255,255,.24); }
.empty { text-align:center; padding:50px; color:#9ca3af; font-size:14px; }
</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>API Routes</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="#" id="navChatRoutes"><span class="icon">💬</span> Conversas</a>
<div class="nav-label admin-only">Administrador</div>
<a href="#" id="navConfigRoutes" class="admin-only"><span class="icon">⚙️</span> Configurações</a>
<a href="#" class="active admin-only" id="navRoutesActive"><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">📡 Rotas da API</span>
<span id="totalRotas" style="font-size:13px;color:#6b7280"></span>
</div>
<div class="container">
<div class="search-box">
<input type="text" id="searchInput" placeholder="Buscar por método, path ou descrição..." oninput="filtrarRotas()">
<select id="filterCat" onchange="filtrarRotas()"><option value="">Todas as categorias</option></select>
<select id="filterMethod" onchange="filtrarRotas()">
<option value="">Todos os métodos</option>
<option value="GET">GET</option><option value="POST">POST</option>
<option value="PUT">PUT</option><option value="DELETE">DELETE</option>
</select>
<select id="filterAuth" onchange="filtrarRotas()">
<option value="">Pública e protegida</option>
<option value="yes">🔒 Requer token</option>
<option value="no">🔓 Pública</option>
</select>
<button class="btn-ghost" onclick="expandirTodas(true)">Expandir</button>
<button class="btn-ghost" onclick="expandirTodas(false)">Recolher</button>
<button class="btn-ghost" onclick="carregarRotas()">🔄 Atualizar</button>
</div>
<div class="legenda">
<span class="pill"><span class="auth-badge auth-yes">🔒 Token</span> requer <code>Authorization: Bearer &lt;token&gt;</code></span>
<span class="pill"><span class="auth-badge auth-no">🔓 Público</span> sem autenticação</span>
<span class="pill"><span class="tag req">obrigatório</span> / <span class="tag opt">opcional</span></span>
</div>
<div id="statusMsg" class="empty">Carregando rotas...</div>
<div id="rotasContainer"></div>
</div>
</div>
<script>
(function(){
'use strict';
const token = localStorage.getItem('chatc2_token');
const alias = localStorage.getItem('chatc2_alias') || 'novo_local';
const user = JSON.parse(localStorage.getItem('chatc2_user') || '{}');
if (!token) { window.location.href = '/app/' + alias + '/login'; return; }
document.getElementById("navDashboard").onclick = function(e){ e.preventDefault(); window.location.href = "/app/" + alias + "/dashboard"; };
document.getElementById("navClients").onclick = function(e){ e.preventDefault(); window.location.href = "/app/" + alias + "/clients"; };
document.getElementById("navChatRoutes").onclick = function(e){ e.preventDefault(); window.location.href = "/app/" + alias + "/company/" + (user.empresas?.[0] || 1) + "/conversation/0"; };
document.getElementById("navConfigRoutes").onclick = function(e){ e.preventDefault(); window.location.href = "/app/" + alias + "/settings"; };
document.getElementById("navRoutesActive").onclick = function(e){ e.preventDefault(); window.location.href = "/app/" + alias + "/routes"; };
if ((user.tipoChat || "A") === "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';
};
var todasRotas = [];
function esc(s){ return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// Realça os {placeholders} no path; troca {alias} pelo alias real
function fmtPath(path){
return esc(path.replace(/\{alias\}/g, alias))
.replace(/\{([a-zA-Z_]+)\}/g, '<i class="ph">{$1}</i>');
}
function copyBtn(texto, light){
return '<button class="copy' + (light ? ' copy-light' : '') + '" data-copy="' +
encodeURIComponent(texto) + '">📋 Copiar</button>';
}
function tabelaParams(titulo, itens){
if (!itens || !itens.length) return '';
var linhas = itens.map(function(it){
return '<tr><td class="pname">' + esc(it.nome) + '</td><td>' +
(it.obrigatorio ? '<span class="tag req">obrigatório</span>' : '<span class="tag opt">opcional</span>') +
'</td><td>' + esc(it.desc) + '</td></tr>';
}).join('');
return '<div class="block"><div class="block-title">' + titulo + '</div>' +
'<table class="param-table"><thead><tr><th>Nome</th><th>Obrigatório</th><th>Descrição</th></tr></thead>' +
'<tbody>' + linhas + '</tbody></table></div>';
}
function blocoBody(r){
if (!r.body) return '';
var json = JSON.stringify(r.body, null, 2);
var reqNote = (r.bodyReq && r.bodyReq.length)
? 'Obrigatórios: ' + r.bodyReq.map(function(f){ return '<code>' + esc(f) + '</code>'; }).join(', ')
: 'Todos os campos do exemplo são opcionais.';
return '<div class="block"><div class="block-title">Body (JSON) ' + copyBtn(json) + '</div>' +
'<pre class="code-json">' + esc(json) + '</pre>' +
'<div class="req-note">' + reqNote + '</div></div>';
}
function exemploCurl(r){
if (r.pagina) return '';
var url = window.location.origin + r.path.replace(/\{alias\}/g, alias);
var p = ['curl -X ' + r.method + ' "' + url + '"'];
if (r.auth) p.push('-H "Authorization: Bearer <SEU_TOKEN>"');
if (r.body) { p.push('-H "Content-Type: application/json"'); p.push("-d '" + JSON.stringify(r.body) + "'"); }
var cmd = p.join(' \\\n ');
return '<div class="block"><div class="block-title">Exemplo (cURL) ' + copyBtn(cmd, true) + '</div>' +
'<pre class="code-curl">' + esc(cmd) + '</pre></div>';
}
function cardRota(r){
var authBadge = r.auth
? '<span class="auth-badge auth-yes">🔒 Token</span>'
: '<span class="auth-badge auth-no">🔓 Público</span>';
var detalhe = '';
detalhe += '<div class="note-box">' + (r.auth
? 'Requer cabeçalho <code>Authorization: Bearer &lt;token&gt;</code>.'
: 'Rota pública — não precisa de token.') +
(r.pagina ? ' Retorna uma página HTML (abre no navegador).' : '') + '</div>';
if (r.desc) detalhe = '<div class="block"><div class="block-title">Descrição</div>' + esc(r.desc) + '</div>' + detalhe;
detalhe += tabelaParams('Parâmetros de URL', r.params);
detalhe += tabelaParams('Parâmetros de query', r.query);
detalhe += blocoBody(r);
if (r.note) detalhe += '<div class="block"><div class="note-box">️ ' + esc(r.note) + '</div></div>';
detalhe += exemploCurl(r);
return '<details class="route">' +
'<summary class="route-head">' +
'<span class="chev"></span>' +
'<span class="method ' + r.method.toLowerCase() + '">' + r.method + '</span>' +
'<span class="route-path">' + fmtPath(r.path) + '</span>' +
authBadge +
(r.desc ? '<span class="route-summary-desc">' + esc(r.desc) + '</span>' : '') +
'</summary>' +
'<div class="route-detail">' + detalhe + '</div>' +
'</details>';
}
function categoriasUnicas(rotas){
var cats = {};
rotas.forEach(function(r){ if (r.cat) cats[r.cat] = true; });
return Object.keys(cats).sort();
}
async function carregarRotas(){
var statusEl = document.getElementById('statusMsg');
var container = document.getElementById('rotasContainer');
statusEl.style.display = 'block';
statusEl.textContent = 'Carregando rotas...';
container.innerHTML = '';
try {
var res = await fetch('/api/routes');
var json = await res.json();
if (!json.success || !json.data) {
statusEl.textContent = '❌ Erro ao carregar rotas: ' + (json.error || 'resposta inválida');
return;
}
todasRotas = json.data;
var selectCat = document.getElementById('filterCat');
var cats = categoriasUnicas(todasRotas);
selectCat.innerHTML = '<option value="">Todas as categorias</option>' +
cats.map(function(c){ return '<option value="' + c.replace(/"/g,'&quot;') + '">' + c + '</option>'; }).join('');
statusEl.style.display = 'none';
renderRotas();
} catch(e){
statusEl.textContent = '❌ Erro ao carregar: ' + e.message;
}
}
function renderRotas(){
var container = document.getElementById('rotasContainer');
var q = (document.getElementById('searchInput').value || '').toLowerCase();
var catFiltro = document.getElementById('filterCat').value;
var methodFiltro = document.getElementById('filterMethod').value;
var authFiltro = document.getElementById('filterAuth').value;
var filtradas = todasRotas.filter(function(r){
if (catFiltro && r.cat !== catFiltro) return false;
if (methodFiltro && r.method !== methodFiltro) return false;
if (authFiltro === 'yes' && !r.auth) return false;
if (authFiltro === 'no' && r.auth) return false;
if (q) {
return r.method.toLowerCase().includes(q) ||
r.path.toLowerCase().includes(q) ||
(r.desc || '').toLowerCase().includes(q);
}
return true;
});
var grupos = {};
filtradas.forEach(function(r){
var cat = r.cat || '📦 Outros';
(grupos[cat] = grupos[cat] || []).push(r);
});
var html = '';
Object.keys(grupos).sort().forEach(function(cat){
var rotas = grupos[cat];
html += '<div class="cat-group"><div class="cat-title">' + cat +
' <span class="count">' + rotas.length + '</span></div>' +
rotas.map(cardRota).join('') + '</div>';
});
container.innerHTML = html || '<div class="empty">Nenhuma rota encontrada com os filtros atuais.</div>';
document.getElementById('totalRotas').textContent = filtradas.length + ' de ' + todasRotas.length + ' rotas';
}
window.filtrarRotas = function(){ renderRotas(); };
window.expandirTodas = function(abrir){
document.querySelectorAll('#rotasContainer details').forEach(function(d){ d.open = abrir; });
};
// Cópia (delegação) — funciona para path, body e cURL
document.addEventListener('click', function(ev){
var btn = ev.target.closest('.copy');
if (!btn) return;
ev.preventDefault();
var texto = decodeURIComponent(btn.getAttribute('data-copy') || '');
navigator.clipboard.writeText(texto).then(function(){
var orig = btn.innerHTML;
btn.innerHTML = '✅ Copiado';
setTimeout(function(){ btn.innerHTML = orig; }, 1400);
});
});
carregarRotas();
})();
</script>
<script src="/js/dark-mode.js"></script>
</body>
</html>
+858
View File
@@ -0,0 +1,858 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configurações - 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; }
.tabs { display:flex; margin-bottom:24px; background:#fff; border-radius:12px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,0.08); border:1px solid #f3f4f6; }
.tabs button { flex:1; padding:13px 16px; border:none; background:#fff; font-size:14px; cursor:pointer; border-bottom:2px solid transparent; transition:all .15s; color:#6b7280; font-weight:500; }
.tabs button:hover { background:#f9fafb; color:#374151; }
.tabs button.active { border-bottom-color:#667eea; color:#667eea; font-weight:700; background:#fafbff; }
.tab-content { display:none; }
.tab-content.active { display:block; }
.form-group { margin-bottom:16px; }
.form-group label { display:block; font-size:13px; font-weight:600; color:#374151; margin-bottom:5px; }
.form-group input, .form-group select, .form-group textarea { width:100%; padding:10px 14px; border:2px solid #e5e7eb; border-radius:8px; font-size:14px; outline:none; background:#f9fafb; color:#111827; transition:all .15s; font-family:inherit; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color:#667eea; background:#fff; box-shadow:0 0 0 3px rgba(102,126,234,0.1); }
.form-group textarea { min-height:80px; resize:vertical; }
.form-group .toggle { display:flex; align-items:center; gap:8px; }
.form-group .toggle input { width:auto; }
.btn { display:inline-flex; align-items:center; gap:6px; padding:10px 20px; border:none; border-radius:8px; font-size:14px; font-weight:600; cursor:pointer; transition:all .15s; }
.btn-primary { background:#667eea; color:#fff; }
.btn-primary:hover { background:#5a67d8; }
.btn-danger { background:#ef4444; color:#fff; }
.btn-danger:hover { background:#dc2626; }
.btn-sm { padding:6px 12px; font-size:12px; border-radius:6px; }
.badge { display:inline-block; padding:2px 9px; border-radius:10px; font-size:11px; font-weight:600; }
.badge-success { background:#d1fae5; color:#065f46; }
.badge-danger { background:#fef2f2; color:#991b1b; }
.user-info { display:flex; align-items:center; gap:14px; }
.user-name { font-size:14px; font-weight:500; color:#374151; }
.status-badge { display:inline-flex; align-items:center; gap:5px; padding:4px 12px; border-radius:20px; font-size:12px; font-weight:600; }
.status-badge.online { background:#d1fae5; color:#065f46; }
</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="navClientsSide"><span class="icon">👥</span> Clientes</a>
<a href="#" id="navChat"><span class="icon">💬</span> Conversas</a>
<div class="nav-label admin-only" id="adminLabelSet">Administrador</div>
<a href="#" class="active admin-only" id="navConfig"><span class="icon">⚙️</span> Configurações</a>
<a href="#" id="navRoutesSet" 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">⚙️ Configurações</span></div>
<div class="container">
<div class="tabs">
<button class="active" onclick="ativarAba('equipe',this)">👥 Equipe</button>
<button onclick="ativarAba('fluxo',this)">📋 Fluxo</button>
<button onclick="ativarAba('empresa',this)">🏢 Empresa</button>
<button onclick="ativarAba('etiquetas',this)">🏷️ Etiquetas</button>
<button onclick="ativarAba('conexao',this)">📱 Conexão</button>
</div>
<!-- Aba Fluxo -->
<div class="tab-content" id="tabFluxo">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3 style="margin:0">📋 Fluxo de Atendimento</h3>
</div>
<p style="color:#6b7280;font-size:13px;margin-bottom:16px">
Configure os submenus que aparecem quando o cliente seleciona uma equipe.
As opções iniciais são sempre as <strong>Equipes</strong> cadastradas,
com a opção de <strong>Segunda via de Boleto</strong> no final.
</p>
<div id="menusList"><p style="color:#9ca3af">Carregando...</p></div>
</div>
</div>
<!-- Aba Equipe -->
<div class="tab-content active" id="tabEquipe">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3 style="margin:0">Equipes</h3>
<button class="btn btn-primary btn-sm" onclick="mostrarModalEquipe()">+ Nova Equipe</button>
</div>
<div id="equipesList"><p style="color:#9ca3af">Carregando...</p></div>
</div>
<div class="card">
<h3>Usuários da Empresa</h3>
<div id="usuariosList"><p style="color:#9ca3af">Carregando...</p></div>
</div>
</div>
<!-- Aba Empresa -->
<div class="tab-content" id="tabEmpresa">
<div class="card">
<h3>Configurações da Empresa</h3>
<div id="configForm"><p style="color:#9ca3af">Carregando...</p></div>
</div>
</div>
<!-- Aba Etiquetas -->
<div class="tab-content" id="tabEtiquetas">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3 style="margin:0">Etiquetas</h3>
<button class="btn btn-primary btn-sm" onclick="mostrarModalEtiqueta()">+ Nova Etiqueta</button>
</div>
<div id="etiquetasList"><p style="color:#9ca3af">Carregando...</p></div>
</div>
</div>
<!-- Aba Conexão -->
<div class="tab-content" id="tabConexao">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h3 style="margin:0">📱 Conexões WhatsApp</h3>
<button class="btn btn-primary btn-sm" onclick="mostrarModalConexao()">+ Nova Conexão</button>
</div>
<div id="conexoesList"><p style="color:#9ca3af">Carregando...</p></div>
</div>
</div>
</div>
</div>
<!-- Modal Equipe -->
<div class="modal-overlay" id="modalEquipe" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:400px">
<h3 id="modalEquipeTitle">Nova Equipe</h3>
<input type="hidden" id="editEquipeId">
<div class="form-group"><label>Nome da Equipe</label><input type="text" id="equipeNome" placeholder="Ex: Atendimento"></div>
<div class="form-group"><label>Ordem</label><input type="number" id="equipeOrdem" value="0" min="0" style="width:80px"><span style="font-size:12px;color:#9ca3af;margin-left:8px">(menor = aparece primeiro)</span></div>
<div class="form-group"><label>Membros</label><div id="equipeMembros"></div></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn" onclick="fecharModal('modalEquipe')" style="background:#f3f4f6">Cancelar</button>
<button class="btn btn-primary" onclick="salvarEquipe()">Salvar</button>
</div>
</div>
</div>
<!-- Modal Etiqueta -->
<div class="modal-overlay" id="modalEtiqueta" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:400px">
<h3 id="modalEtiquetaTitle">Nova Etiqueta</h3>
<input type="hidden" id="editEtiquetaId">
<div class="form-group"><label>Nome</label><input type="text" id="etiquetaNome" placeholder="Ex: Cliente VIP"></div>
<div class="form-group"><label>Cor</label><input type="color" id="etiquetaCor" value="#667eea"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn" onclick="fecharModal('modalEtiqueta')" style="background:#f3f4f6">Cancelar</button>
<button class="btn btn-primary" onclick="salvarEtiqueta()">Salvar</button>
</div>
</div>
</div>
<!-- Modal Menu -->
<div class="modal-overlay" id="modalMenu" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:500px">
<h3 id="modalMenuTitle">Novo Submenu</h3>
<input type="hidden" id="editMenuId">
<input type="hidden" id="editMenuEquipeId">
<input type="hidden" id="editMenuPaiId">
<div class="form-group" id="menuEquipeGroup"><label>Equipe</label><select id="menuEquipe"><option value="">Selecione...</option></select></div>
<div class="form-group"><label>Título</label><input type="text" id="menuTitulo" placeholder="Ex: Alteração Cadastral"></div>
<div class="form-group">
<label>Tipo</label>
<select id="menuTipo" onchange="mudarTipoMenu()">
<option value="M">📂 Submenu (mostra mais opções)</option>
<option value="T">💬 Texto (envia mensagem fixa)</option>
<option value="R">⚙️ Ação (executa alteração via API)</option>
</select>
</div>
<div class="form-group" id="menuTextoGroup" style="display:none"><label>Texto a ser enviado</label><textarea id="menuTexto" rows="3" placeholder="Digite a mensagem..."></textarea></div>
<div class="form-group" id="menuRotaGroup" style="display:none">
<label>Ação (Rota)</label>
<select id="menuAcaoRota">
<option value="">Selecione uma ação...</option>
<option value="listar_carnes">📄 Listar carnês / Boleto</option>
<option value="alterar_email">📧 Alterar E-mail</option>
<option value="alterar_celular">📱 Alterar Celular</option>
<option value="alterar_endereco">🏠 Alterar Endereço</option>
<option value="info_cliente">️ Mostrar dados do cliente</option>
</select>
<div style="margin-top:8px"><label>Pergunta ao cliente (se precisar de dados)</label>
<textarea id="menuAcaoPrompt" rows="2" placeholder="Ex: Informe seu novo e-mail:" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:13px;resize:vertical"></textarea></div>
</div>
<div class="form-group"><label>Ordem</label><input type="number" id="menuOrdem" value="0" min="0" style="width:80px"></div>
<div class="form-group"><label>Subordinado a (opcional)</label><select id="menuPai"><option value="">Nenhum (raiz da equipe)</option></select></div>
<div class="form-group"><label>Etiquetas (adicionar ao selecionar)</label><div id="menuEtiquetasContainer" style="display:flex;flex-wrap:wrap;gap:6px;padding:6px;border:1px solid #e5e7eb;border-radius:8px;min-height:36px"></div></div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:12px">
<button class="btn" onclick="fecharModal('modalMenu')" style="background:#f3f4f6">Cancelar</button>
<button class="btn btn-primary" onclick="salvarMenu()">Salvar</button>
</div>
</div>
</div>
<!-- Modal Conexão -->
<div class="modal-overlay" id="modalConexao" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;z-index:1000">
<div style="background:#fff;border-radius:12px;padding:24px;width:90%;max-width:500px">
<h3 id="modalConexaoTitle">Nova Conexão WhatsApp</h3>
<input type="hidden" id="editConexaoId">
<div class="form-group"><label>Nome da Instância</label><input type="text" id="conNome" placeholder="Ex: WhatsApp Comercial"></div>
<div class="form-group"><label>URL Evolution API</label><input type="text" id="conUrl" value="https://evoatende.c2sistemas.com.br" placeholder="https://evoatende.c2sistemas.com.br"></div>
<div class="form-group"><label>API Key</label><input type="text" id="conApiKey" placeholder="API Key"></div>
<div class="form-group"><label>Nome da Instância (Evolution)</label><input type="text" id="conInstance" placeholder="Ex: empresa1-wpp"></div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn" onclick="fecharModal('modalConexao')" style="background:#f3f4f6">Cancelar</button>
<button class="btn btn-primary" onclick="salvarConexao()" id="btnSalvarConexao">Conectar</button>
</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;
document.getElementById("navDashboard").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/dashboard"; };
document.getElementById("navChat").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/company/" + empresaId + "/conversation/0"; };
document.getElementById("navConfig").onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/settings"; };
var navClientsSide = document.getElementById("navClientsSide");
if (navClientsSide) navClientsSide.onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/clients"; };
var navRoutesSet = document.getElementById("navRoutesSet");
if (navRoutesSet) navRoutesSet.onclick = function(e) { e.preventDefault(); window.location.href = "/app/" + alias + "/routes"; };
var tc = user.tipoChat || "A";
if (tc === "G") {
var admins = document.querySelectorAll(".admin-only");
for (var i = 0; i < admins.length; i++) admins[i].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, opts) {
return fetch('/api/' + alias + path, Object.assign({
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }
}, opts)).then(function(r) { return r.json(); });
}
// ===== ABAS =====
window.ativarAba = function(aba, btn) {
document.querySelectorAll('.tabs button').forEach(function(b) { b.classList.remove('active'); });
document.querySelectorAll('.tab-content').forEach(function(t) { t.classList.remove('active'); });
btn.classList.add('active');
document.getElementById('tab' + aba.charAt(0).toUpperCase() + aba.slice(1)).classList.add('active');
};
// ===== EQUIPES =====
async function carregarEquipes() {
var data = await api('/teams?empresaId=' + empresaId);
var div = document.getElementById('equipesList');
if (!data.success || !data.data || data.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Nenhuma equipe cadastrada</p>';
return;
}
div.innerHTML = '<table><thead><tr><th>Ordem</th><th>Nome</th><th>Membros</th><th>Ações</th></tr></thead><tbody>' +
data.data.map(function(eq) {
var membros = (eq.membros || []).map(function(m) { return m.nome; }).join(', ') || '-';
return '<tr><td>' + (eq.ordem || 0) + '</td><td><strong>' + eq.nome + '</strong></td><td>' + membros + '</td><td>' +
'<button class="btn btn-sm" onclick="editarEquipe(' + eq.id + ',' + (eq.ordem || 0) + ',\'' + eq.nome.replace(/'/g,"\\'") + '\')" style="background:#f3f4f6;margin-right:4px">✏️</button>' +
'<button class="btn btn-sm btn-danger" onclick="excluirEquipe(' + eq.id + ')">🗑️</button></td></tr>';
}).join('') + '</tbody></table>';
}
window.mostrarModalEquipe = function() {
document.getElementById('editEquipeId').value = '';
document.getElementById('equipeOrdem').value = 0;
document.getElementById('equipeNome').value = '';
document.getElementById('modalEquipeTitle').textContent = 'Nova Equipe';
carregarUsuariosCheckbox();
document.getElementById('modalEquipe').style.display = 'flex';
};
window.editarEquipe = function(id, ordem, nome) {
document.getElementById('editEquipeId').value = id;
document.getElementById('equipeOrdem').value = ordem;
document.getElementById('equipeNome').value = nome;
document.getElementById('modalEquipeTitle').textContent = 'Editar Equipe';
carregarUsuariosCheckbox(id);
document.getElementById('modalEquipe').style.display = 'flex';
};
async function carregarUsuariosCheckbox(equipeId) {
var div = document.getElementById('equipeMembros');
var data = await api('/company/users?empresaId=' + empresaId);
if (!data.success) { div.innerHTML = '<p style="color:#9ca3af">Erro ao carregar</p>'; return; }
var membrosAtuais = [];
if (equipeId) {
var eqData = await api('/teams?empresaId=' + empresaId);
var eq = (eqData.data || []).find(function(e) { return e.id === equipeId; });
if (eq) membrosAtuais = (eq.membros || []).map(function(m) { return m.id; });
}
div.innerHTML = (data.data || []).map(function(u) {
var checked = membrosAtuais.includes(u.id) ? 'checked' : '';
return '<label style="display:flex;align-items:center;gap:8px;padding:6px 0;font-size:13px"><input type="checkbox" value="' + u.id + '" ' + checked + '> ' + u.nome + ' (' + u.login + ')</label>';
}).join('');
}
window.salvarEquipe = async function() {
var id = document.getElementById('editEquipeId').value;
var nome = document.getElementById('equipeNome').value.trim();
var ordem = parseInt(document.getElementById('equipeOrdem').value) || 0;
var membros = Array.from(document.querySelectorAll('#equipeMembros input:checked')).map(function(cb) { return parseInt(cb.value); });
if (!nome) return alert('Informe o nome da equipe');
if (id) {
var r = await api('/teams/' + id, { method: 'PUT', body: JSON.stringify({ nome: nome, ordem: ordem, membros: membros }) });
if (r.success) { fecharModal('modalEquipe'); carregarEquipes(); }
} else {
var r = await api('/teams', { method: 'POST', body: JSON.stringify({ nome: nome, ordem: ordem, membros: membros, empresaId: empresaId }) });
if (r.success) { fecharModal('modalEquipe'); carregarEquipes(); }
}
};
window.excluirEquipe = async function(id) {
if (!confirm('Excluir esta equipe?')) return;
var r = await api('/teams/' + id, { method: 'DELETE' });
if (r.success) carregarEquipes();
};
// ===== USUÁRIOS =====
async function carregarUsuarios() {
var data = await api('/company/users?empresaId=' + empresaId);
var div = document.getElementById('usuariosList');
if (!data.success || !data.data || data.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Nenhum usuário encontrado</p>';
return;
}
div.innerHTML = '<table><thead><tr><th>Nome</th><th>Login</th><th>Tipo Chat</th></tr></thead><tbody>' +
data.data.map(function(u) {
var tipo = u.tipoChat === 'A' ? '<span class="badge badge-success">Atendente</span>' : u.tipoChat === 'G' ? '<span class="badge badge-success">Gerente</span>' : '<span class="badge badge-danger">Bloqueado</span>';
return '<tr><td>' + u.nome + '</td><td>' + u.login + '</td><td>' + tipo + '</td></tr>';
}).join('') + '</tbody></table>';
}
// ===== FLUXO DE ATENDIMENTO (MENUS) =====
async function carregarMenus() {
var [eqData, menuData] = await Promise.all([
api('/teams?empresaId=' + empresaId),
api('/menus?empresaId=' + empresaId)
]);
var div = document.getElementById('menusList');
if (!eqData.success || !eqData.data || eqData.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Crie equipes primeiro para configurar os submenus.</p>';
return;
}
var menusAgrupados = menuData.success ? menuData.data : [];
var html = '';
eqData.data.forEach(function(eq) {
var menusEq = menusAgrupados.find(function(g) { return g.equipeId === eq.id; });
var temMenus = menusEq && menusEq.menus && menusEq.menus.length > 0;
html += '<div style="margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden">';
html += '<div style="background:#f9fafb;padding:10px 14px;font-weight:600;font-size:14px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between">';
html += '<span>👥 ' + eq.nome + '</span>';
html += '<button class="btn btn-sm" onclick="mostrarModalMenu(' + eq.id + ')" style="background:#e0e7ff">+ Submenu</button>';
html += '</div>';
if (temMenus) {
html += '<div style="padding:4px 0">' + montarArvoreHtml(menusEq.menus, 0) + '</div>';
} else {
html += '<p style="padding:12px 14px;color:#9ca3af;font-size:13px;margin:0">Nenhum submenu configurado.</p>';
}
html += '</div>';
});
div.innerHTML = html;
}
function montarArvoreHtml(menus, nivel) {
if (!menus || menus.length === 0) return '';
var html = '<div style="padding-left:' + (nivel * 20 + 8) + 'px">';
menus.forEach(function(m) {
var tipoIcon = m.tipo === 'M' ? '📂' : m.tipo === 'T' ? '💬' : m.tipo === 'R' ? '⚙️' : '❓';
html += '<div style="display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid #f3f4f6">';
html += '<span style="flex:1;font-size:13px">' + tipoIcon + ' ' + m.titulo + ' <span style="color:#9ca3af;font-size:11px">(' + m.tipo + ')</span></span>';
html += '<button class="btn btn-sm" onclick="mostrarModalMenu(' + m.equipeId + ',' + m.id + ')" style="background:#f3f4f6;margin-right:4px;padding:4px 8px;font-size:11px">✏️</button>';
html += '<button class="btn btn-sm" onclick="excluirMenu(' + m.id + ',\'' + m.titulo.replace(/'/g,"\\'") + '\')" style="background:#fef2f2;color:#991b1b;padding:4px 8px;font-size:11px">🗑️</button>';
html += '<button class="btn btn-sm" onclick="mostrarModalMenu(' + m.equipeId + ',null,' + m.id + ')" style="margin-left:4px;padding:4px 8px;font-size:11px"> Sub</button>';
html += '</div>';
if (m.filhos && m.filhos.length > 0) {
html += montarArvoreHtml(m.filhos, nivel + 1);
}
});
html += '</div>';
return html;
}
window.mostrarModalMenu = async function(equipeId, editMenuId, parentMenuId) {
// Carrega equipes para o select
var eqData = await api('/teams?empresaId=' + empresaId);
var selectEq = document.getElementById('menuEquipe');
selectEq.innerHTML = '<option value="">Selecione...</option>';
(eqData.data || []).forEach(function(eq) {
var opt = document.createElement('option');
opt.value = eq.id;
opt.textContent = eq.nome;
if (eq.id === equipeId) opt.selected = true;
selectEq.appendChild(opt);
});
document.getElementById('editMenuId').value = editMenuId || '';
document.getElementById('editMenuEquipeId').value = equipeId || '';
document.getElementById('editMenuPaiId').value = parentMenuId || '';
document.getElementById('menuTitulo').value = '';
document.getElementById('menuTipo').value = 'M';
document.getElementById('menuTexto').value = '';
document.getElementById('menuOrdem').value = 0;
document.getElementById('menuAcaoRota').value = '';
document.getElementById('menuAcaoPrompt').value = '';
document.getElementById('menuTextoGroup').style.display = 'none';
document.getElementById('menuRotaGroup').style.display = 'none';
document.getElementById('menuEquipeGroup').style.display = parentMenuId ? 'none' : 'block';
// Carrega menus pai disponíveis
var selectPai = document.getElementById('menuPai');
selectPai.innerHTML = '<option value="">Nenhum (raiz da equipe)</option>';
if (!parentMenuId) {
var flatData = await api('/menus/flat?empresaId=' + empresaId + '&equipeId=' + equipeId);
if (flatData.success && flatData.data) {
flatData.data.forEach(function(m) {
if (m.id !== editMenuId) {
var opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.titulo;
selectPai.appendChild(opt);
}
});
}
}
// Carrega etiquetas disponiveis como checkboxes
var etiquetasData = await api('/labels?empresaId=' + empresaId);
var etiquetasSelecionadas = editMenuId ? await buscarEtiquetasDoMenu(editMenuId) : [];
if (!parentMenuId) etiquetasSelecionadas = etiquetasSelecionadas || [];
renderEtiquetasCheckbox(etiquetasData, etiquetasSelecionadas);
if (editMenuId) {
// Modo edição: busca dados
document.getElementById('modalMenuTitle').textContent = 'Editar Submenu';
// Como a API retorna agrupado, precisamos buscar os dados de outra forma
// Vamos usar uma chamada direta
var allData = await api('/menus?empresaId=' + empresaId);
if (allData.success) {
var found = null;
allData.data.forEach(function(g) {
function buscar(m, id) {
if (m.id === id) return m;
if (m.filhos) {
for (var f of m.filhos) {
var r = buscar(f, id);
if (r) return r;
}
}
return null;
}
(g.menus || []).forEach(function(m) {
var r = buscar(m, editMenuId);
if (r) found = r;
});
});
if (found) {
document.getElementById('menuTitulo').value = found.titulo || '';
document.getElementById('menuTipo').value = found.tipo || 'M';
document.getElementById('menuTexto').value = found.texto || '';
document.getElementById('menuOrdem').value = found.ordem || 0;
document.getElementById('menuAcaoRota').value = found.acaoRota || '';
document.getElementById('menuAcaoPrompt').value = found.acaoPrompt || '';
if (found.paiId) {
selectPai.value = found.paiId;
}
if (found.tipo === 'T') {
document.getElementById('menuTextoGroup').style.display = 'block';
document.getElementById('menuRotaGroup').style.display = 'none';
} else if (found.tipo === 'R') {
document.getElementById('menuTextoGroup').style.display = 'none';
document.getElementById('menuRotaGroup').style.display = 'block';
}
}
}
} else {
document.getElementById('modalMenuTitle').textContent = 'Novo Submenu';
// Se tem um pai, marca no select
if (parentMenuId) {
selectPai.value = parentMenuId;
}
}
document.getElementById('modalMenu').style.display = 'flex';
};
window.mudarTipoMenu = function() {
var tipo = document.getElementById('menuTipo').value;
document.getElementById('menuTextoGroup').style.display = tipo === 'T' ? 'block' : 'none';
document.getElementById('menuRotaGroup').style.display = tipo === 'R' ? 'block' : 'none';
};
// Busca etiquetas configuradas em um menu (percorre a arvore da API)
async function buscarEtiquetasDoMenu(menuId) {
var allData = await api('/menus?empresaId=' + empresaId);
console.log('[Menu] buscarEtiquetasDoMenu - menuId:', menuId, '| allData:', JSON.stringify(allData).substring(0, 300));
var ids = [];
function percorrer(lista) {
(lista || []).forEach(function(m) {
if (m.id === menuId) {
if (m.etiquetaIds) ids = m.etiquetaIds.split(',').map(function(x){return parseInt(x.trim());}).filter(function(x){return !isNaN(x);});
}
if (m.filhos) percorrer(m.filhos);
});
}
allData.data.forEach(function(g) { percorrer(g.menus); });
return ids;
}
// Renderiza checkboxes de etiquetas no modal
function renderEtiquetasCheckbox(data, selecionadas) {
var container = document.getElementById('menuEtiquetasContainer');
if (!data.success || !data.data || data.data.length === 0) {
container.innerHTML = '<span style="color:#9ca3af;font-size:12px">Nenhuma etiqueta cadastrada</span>';
return;
}
container.innerHTML = data.data.map(function(e) {
var checked = selecionadas.includes(e.id) ? 'checked' : '';
return '<label style="display:flex;align-items:center;gap:4px;padding:4px 8px;background:#f3f4f6;border-radius:6px;font-size:12px;cursor:pointer">' +
'<input type="checkbox" value="' + e.id + '" ' + checked + ' style="accent-color:#667eea"> ' +
'<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + (e.cor || '#667eea') + '"></span> ' +
e.nome +
'</label>';
}).join('');
}
window.salvarMenu = async function() {
var editId = document.getElementById('editMenuId').value;
var equipeId = document.getElementById('editMenuEquipeId').value || document.getElementById('menuEquipe').value;
var paiId = document.getElementById('editMenuPaiId').value || document.getElementById('menuPai').value;
var titulo = document.getElementById('menuTitulo').value.trim();
var tipo = document.getElementById('menuTipo').value;
var texto = document.getElementById('menuTexto').value;
var ordem = parseInt(document.getElementById('menuOrdem').value) || 0;
var acaoRota = document.getElementById('menuAcaoRota').value;
var acaoPrompt = document.getElementById('menuAcaoPrompt').value;
// Etiquetas selecionadas
var etiquetaIds = Array.from(document.querySelectorAll('#menuEtiquetasContainer input[type="checkbox"]:checked')).map(function(cb) { return parseInt(cb.value); }).filter(function(v) { return !isNaN(v); }).join(',');
if (!titulo) return alert('Informe o título');
if (!equipeId) return alert('Selecione a equipe');
var body = {
empresaId: empresaId,
equipeId: parseInt(equipeId),
titulo: titulo,
tipo: tipo,
texto: tipo === 'T' ? texto : null,
ordem: ordem,
acaoRota: tipo === 'R' ? acaoRota : null,
acaoPrompt: tipo === 'R' ? acaoPrompt : null,
menuPaiId: paiId ? parseInt(paiId) : null,
etiquetaIds: etiquetaIds || null,
};
console.log('[Menu] Salvando body:', JSON.stringify(body));
if (editId) {
var r = await api('/menus/' + editId, { method: 'PUT', body: JSON.stringify(body) });
console.log('[Menu] Resposta save:', JSON.stringify(r));
if (r.success) { fecharModal('modalMenu'); carregarMenus(); }
else alert('Erro: ' + (r.error || 'desconhecido'));
} else {
var r = await api('/menus', { method: 'POST', body: JSON.stringify(body) });
if (r.success) { fecharModal('modalMenu'); carregarMenus(); }
else alert('Erro: ' + (r.error || 'desconhecido'));
}
};
window.excluirMenu = async function(id, titulo) {
if (!confirm('Excluir o submenu "' + titulo + '" e todos os seus sub-itens?')) return;
var r = await api('/menus/' + id, { method: 'DELETE' });
if (r.success) carregarMenus();
};
// ===== ETIQUETAS =====
async function carregarEtiquetas() {
var data = await api('/labels?empresaId=' + empresaId);
var div = document.getElementById('etiquetasList');
if (!data.success || !data.data || data.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Nenhuma etiqueta cadastrada</p>';
return;
}
div.innerHTML = '<table><thead><tr><th>Nome</th><th>Cor</th><th>Ações</th></tr></thead><tbody>' +
data.data.map(function(l) {
return '<tr><td><strong>' + l.nome + '</strong></td><td><span class="tag" style="background:' + l.cor + '">' + l.cor + '</span></td><td>' +
'<button class="btn btn-sm" onclick="editarEtiqueta(' + l.id + ',\'' + l.nome.replace(/'/g,"\\'") + '\',\'' + l.cor + '\')" style="background:#f3f4f6;margin-right:4px">✏️</button>' +
'<button class="btn btn-sm btn-danger" onclick="excluirEtiqueta(' + l.id + ')">🗑️</button></td></tr>';
}).join('') + '</tbody></table>';
}
window.mostrarModalEtiqueta = function() {
document.getElementById('editEtiquetaId').value = '';
document.getElementById('etiquetaNome').value = '';
document.getElementById('etiquetaCor').value = '#667eea';
document.getElementById('modalEtiquetaTitle').textContent = 'Nova Etiqueta';
document.getElementById('modalEtiqueta').style.display = 'flex';
};
window.editarEtiqueta = function(id, nome, cor) {
document.getElementById('editEtiquetaId').value = id;
document.getElementById('etiquetaNome').value = nome;
document.getElementById('etiquetaCor').value = cor;
document.getElementById('modalEtiquetaTitle').textContent = 'Editar Etiqueta';
document.getElementById('modalEtiqueta').style.display = 'flex';
};
window.salvarEtiqueta = async function() {
var id = document.getElementById('editEtiquetaId').value;
var nome = document.getElementById('etiquetaNome').value.trim();
var cor = document.getElementById('etiquetaCor').value;
if (!nome) return alert('Informe o nome');
if (id) {
var r = await api('/labels/' + id, { method: 'PUT', body: JSON.stringify({ nome: nome, cor: cor }) });
if (r.success) { fecharModal('modalEtiqueta'); carregarEtiquetas(); }
} else {
var r = await api('/labels', { method: 'POST', body: JSON.stringify({ nome: nome, cor: cor }) });
if (r.success) { fecharModal('modalEtiqueta'); carregarEtiquetas(); }
}
};
window.excluirEtiqueta = async function(id) {
if (!confirm('Excluir esta etiqueta?')) return;
var r = await api('/labels/' + id, { method: 'DELETE' });
if (r.success) carregarEtiquetas();
};
// ===== CONFIGURAÇÕES EMPRESA =====
async function carregarConfig() {
var data = await api('/company/config?empresaId=' + empresaId);
var div = document.getElementById('configForm');
if (!data.success) { div.innerHTML = '<p style="color:#ef4444">Erro ao carregar</p>'; return; }
var cfg = data.data;
div.innerHTML =
'<div class="form-group"><label>Instância Padrão</label><select id="cfgInstancia"><option value="">Nenhuma</option></select></div>' +
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgFoto" ' + (cfg.fotoCelular === 'S' ? 'checked' : '') + '> <label for="cfgFoto">Atualizar foto do cliente conforme WhatsApp</label></div></div>' +
'<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="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>' +
'<div class="form-group"><label>Mensagem após escolha</label><textarea id="cfgTriagemAfter" rows="2" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:13px;resize:vertical">' + (cfg.triagemMsgAfter || '') + '</textarea></div>' +
'<div class="form-group"><label>Número da opção "Segunda via de Boleto"</label><input type="text" id="cfgTriagemBoletoNum" value="' + (cfg.triagemBoletoNumero || '0') + '" style="width:100%;padding:10px;border:2px solid #e5e7eb;border-radius:8px;font-size:14px"></div>' +
'</div>' +
'<div class="form-group"><div class="toggle"><input type="checkbox" id="cfgCsat" ' + (cfg.csatAtivo === 'S' ? 'checked' : '') + '> <label for="cfgCsat">Ativar CSAT (Pesquisa de satisfação)</label></div></div>' +
'<div class="form-group"><label>Mensagem CSAT</label><textarea id="cfgCsatMsg">' + (cfg.csatMensagem || '') + '</textarea></div>' +
'<button class="btn btn-primary" onclick="salvarConfig()">💾 Salvar Configurações</button>';
// Evento para toggle da triagem
document.getElementById('cfgTriagem').addEventListener('change', function() {
document.getElementById('triagemConfig').style.display = this.checked ? 'block' : 'none';
});
// Carrega instâncias no select
var instData = await api('/evolution/instances?empresaId=' + empresaId);
if (instData.success && instData.data) {
var select = document.getElementById('cfgInstancia');
instData.data.forEach(function(ins) {
var opt = document.createElement('option');
opt.value = ins.id;
opt.textContent = ins.nome + ' (' + ins.instanceName + ')';
if (ins.id === cfg.instanciaPadraoId) opt.selected = true;
select.appendChild(opt);
});
}
}
window.salvarConfig = async function() {
var data = {
empresaId: empresaId,
instanciaPadraoId: parseInt(document.getElementById('cfgInstancia').value) || null,
fotoCelular: document.getElementById('cfgFoto').checked ? 'S' : 'N',
saudacaoAtiva: document.getElementById('cfgSaudacao').checked ? 'S' : 'N',
saudacaoMensagem: document.getElementById('cfgSaudacaoMsg').value,
enviarNomeUsuario: document.getElementById('cfgNomeUser').checked ? 'S' : 'N',
triagemAtiva: document.getElementById('cfgTriagem').checked ? 'S' : 'N',
triagemMsgWelcome: document.getElementById('cfgTriagemWelcome').value,
triagemMsgAfter: document.getElementById('cfgTriagemAfter').value,
triagemBoletoNumero: document.getElementById('cfgTriagemBoletoNum').value,
csatAtivo: document.getElementById('cfgCsat').checked ? 'S' : 'N',
csatMensagem: document.getElementById('cfgCsatMsg').value,
};
var r = await api('/company/config', { method: 'POST', body: JSON.stringify(data) });
if (r.success) alert('Configurações salvas com sucesso!');
};
// O addEventListener do cfgTriagem é adicionado dentro do carregarConfig() após criar o HTML
// ===== CONEXÕES =====
async function carregarConexoes() {
var data = await api('/evolution/instances?empresaId=' + empresaId);
var div = document.getElementById('conexoesList');
if (!data.success || !data.data || data.data.length === 0) {
div.innerHTML = '<p style="color:#9ca3af">Nenhuma conexão cadastrada</p>';
return;
}
div.innerHTML = '<table><thead><tr><th>Nome</th><th>Instância</th><th>URL</th><th>Status</th><th>Ações</th></tr></thead><tbody>' +
data.data.map(function(ins) {
var status = ins.status === 'A' ? '<span class="badge badge-success">✅ Conectado</span>' :
ins.status === 'C' ? '<span class="badge badge-success">🟡 Conectando</span>' :
ins.status === 'D' ? '<span class="badge badge-danger">⛔ Desconectado</span>' : '<span class="badge badge-danger">❌ ' + ins.status + '</span>';
var nomeEsc = (ins.nome || '').replace(/'/g, "\\'");
return '<tr><td><strong>' + ins.nome + '</strong></td><td>' + ins.instanceName + '</td><td style="font-size:11px;max-width:150px;overflow:hidden;text-overflow:ellipsis">' + ins.url + '</td><td>' + status + '</td><td>' +
'<button class="btn btn-sm" onclick="editarConexao(' + ins.id + ')" style="background:#f3f4f6;margin-right:4px">✏️</button>' +
'<button class="btn btn-sm" onclick="excluirConexao(' + ins.id + ',\'' + nomeEsc + '\')" style="background:#fef2f2;color:#991b1b;margin-right:4px">🗑️</button>' +
'<button class="btn btn-sm" onclick="gerarQRCode(' + ins.id + ')" style="background:#f3f4f6">📱 QR Code</button></td></tr>';
}).join('') + '</tbody></table>';
}
window.mostrarModalConexao = function() {
document.getElementById('editConexaoId').value = '';
document.getElementById('conNome').value = '';
document.getElementById('conUrl').value = 'https://evoatende.c2sistemas.com.br';
document.getElementById('conApiKey').value = '';
document.getElementById('conInstance').value = '';
document.getElementById('modalConexaoTitle').textContent = 'Nova Conexão WhatsApp';
document.getElementById('btnSalvarConexao').textContent = 'Conectar';
document.getElementById('modalConexao').style.display = 'flex';
};
window.gerarQRCode = async function(id) {
var r = await api('/evolution/qrcode/' + id, { method: 'POST' });
if (r.success && r.data && (r.data.qrcode || r.data.base64)) {
var qr = r.data.qrcode || r.data.base64;
var imgSrc = qr.startsWith('data:') ? qr : 'data:image/png;base64,' + qr;
var w = window.open('', '_blank', 'width=400,height=500');
w.document.write('<html><head><title>QR Code</title></head><body style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:sans-serif">' +
'<h2 style="margin-bottom:20px">Escaneie o QR Code</h2>' +
'<img src="' + imgSrc + '" style="max-width:300px">' +
'<p style="margin-top:20px;color:#6b7280">Abra o WhatsApp no celular e escaneie</p></body></html>');
} else {
alert('Erro ao gerar QR Code: ' + JSON.stringify(r.data));
}
};
// ===== EDITAR CONEXÃO =====
window.editarConexao = async function(id) {
// Busca dados atuais da API
var data = await api('/evolution/instances?empresaId=' + empresaId);
if (!data.success) return alert('Erro ao carregar dados');
var ins = data.data.find(function(i) { return i.id === id; });
if (!ins) return alert('Instância não encontrada');
document.getElementById('editConexaoId').value = id;
document.getElementById('conNome').value = ins.nome;
document.getElementById('conUrl').value = ins.url;
document.getElementById('conApiKey').value = ins.apiKey;
document.getElementById('conInstance').value = ins.instanceName;
document.getElementById('modalConexaoTitle').textContent = 'Editar Conexão WhatsApp';
document.getElementById('btnSalvarConexao').textContent = 'Salvar';
document.getElementById('modalConexao').style.display = 'flex';
};
// ===== EXCLUIR CONEXÃO =====
window.excluirConexao = async function(id, nome) {
if (!confirm('Excluir a conexão "' + nome + '"?')) return;
var r = await api('/evolution/instances/' + id, { method: 'DELETE' });
if (r.success) {
carregarConexoes();
} else {
alert('Erro: ' + r.error);
}
};
window.salvarConexao = async function() {
var editId = document.getElementById('editConexaoId').value;
var data = {
INS_NOME: document.getElementById('conNome').value.trim(),
INS_URL: document.getElementById('conUrl').value.trim(),
INS_API_KEY: document.getElementById('conApiKey').value.trim(),
INS_INSTANCE_NAME: document.getElementById('conInstance').value.trim(),
};
if (!data.INS_NOME || !data.INS_URL || !data.INS_API_KEY || !data.INS_INSTANCE_NAME)
return alert('Preencha todos os campos');
var r;
if (editId) {
// Atualizar
r = await api('/evolution/instances/' + editId, { method: 'PUT', body: JSON.stringify(data) });
} else {
// Criar nova
data.INS_EMPRESA_ID = empresaId;
r = await api('/evolution/connect', { method: 'POST', body: JSON.stringify(data) });
}
if (r.success) {
fecharModal('modalConexao');
document.getElementById('editConexaoId').value = '';
document.getElementById('modalConexaoTitle').textContent = 'Nova Conexão WhatsApp';
document.getElementById('btnSalvarConexao').textContent = 'Conectar';
carregarConexoes();
if (!editId && r.data && r.data.id) gerarQRCode(r.data.id);
} else {
alert('Erro: ' + r.error);
}
};
// ===== MODAIS =====
window.fecharModal = function(id) { document.getElementById(id).style.display = 'none'; };
// Fechar modal ao clicar fora
document.querySelectorAll('.modal-overlay').forEach(function(el) {
el.addEventListener('click', function(e) { if (e.target === this) this.style.display = 'none'; });
});
// ===== INICIAR =====
carregarEquipes();
carregarUsuarios();
carregarMenus();
carregarEtiquetas();
carregarConexoes();
carregarConfig();
})();
</script>
<script src="/js/dark-mode.js"></script>
</body>
</html>