fix: busca server-side + top 20 clientes (antes carregava 261K clientes no browser)
- fetchClientList separado em fetchTopClients (top 20 LIMIT) e fetchClientSearch (LIKE server-side) - Novos endpoints: /admin/api/clientes/top e /admin/api/clientes/search?q= - Cards clicaveis com data-id/data-nome + event delegation (sem inline onclick) - Busca agora faz fetch server-side com debounce 300ms (min 2 chars) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -578,7 +578,7 @@ ${buildFooter()}
|
||||
|
||||
<script>
|
||||
// === State ===
|
||||
var clientList = [], selectedClientId = null, profileData = null, clientData = null;
|
||||
var selectedClientId = null, profileData = null, clientData = null;
|
||||
var currentStart = '${thirtyDaysAgo}', currentEnd = '${today}';
|
||||
var currentPage = 1, pageSize = 25, sortCol = 'date', sortDir = -1, timelineGran = 'D';
|
||||
|
||||
@@ -752,26 +752,29 @@ function renderNetting(data) {
|
||||
document.getElementById('netEffBar').style.width = eff + '%';
|
||||
}
|
||||
|
||||
// === Client Search ===
|
||||
// === Client Search (server-side) ===
|
||||
var _searchTimer = null;
|
||||
function fmtVolShort(v) {
|
||||
if (v >= 1e6) return '$' + (v/1e6).toFixed(1) + 'M';
|
||||
if (v >= 1e3) return '$' + (v/1e3).toFixed(0) + 'K';
|
||||
return '$' + v;
|
||||
}
|
||||
function esc(s) { return s.replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
||||
function loadClients() {
|
||||
fetch('/admin/api/clientes').then(function(r){return r.json();}).then(function(data) {
|
||||
clientList = data;
|
||||
renderTopClients(data.slice(0, 20));
|
||||
fetch('/admin/api/clientes/top').then(function(r){return r.json();}).then(function(data) {
|
||||
renderTopClients(data);
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var deepId = params.get('id');
|
||||
if (deepId) { var m = clientList.find(function(c){return String(c.id)===String(deepId);}); if (m) selectClient(m.id, m.nome); }
|
||||
if (deepId) {
|
||||
var m = data.find(function(c){return String(c.id)===String(deepId);});
|
||||
if (m) { selectClient(m.id, m.nome); }
|
||||
else { fetch('/admin/api/cliente/' + deepId + '/profile').then(function(r){return r.json();}).then(function(p){ if(p&&p.nome) selectClient(deepId, p.nome); }).catch(function(){}); }
|
||||
}
|
||||
}).catch(function(e){ console.error('loadClients:', e); });
|
||||
}
|
||||
function renderTopClients(top) {
|
||||
var el = document.getElementById('topClientsSection');
|
||||
if (!el || !top.length) return;
|
||||
// Group by volume tier
|
||||
var high = [], mid = [], low = [];
|
||||
top.forEach(function(c, i) { c._rank = i + 1; });
|
||||
var maxVol = top[0] ? top[0].vol : 1;
|
||||
@@ -784,26 +787,32 @@ function renderTopClients(top) {
|
||||
function buildCards(list) {
|
||||
return '<div class="top-clients-grid">' + list.map(function(c) {
|
||||
var avgMonth = c.months > 0 ? c.vol / c.months : c.vol;
|
||||
return '<div class="top-client-card" onclick="selectClient(\\''+c.id+'\\',\\''+c.nome.replace(/'/g,"\\\\'")+'\\')"><span class="tc-rank">#'+c._rank+'</span><div class="tc-name" title="'+c.nome.replace(/"/g,'"')+'">'+c.nome+'</div><div class="tc-stats"><span class="tc-vol">'+fmtVolShort(c.vol)+'</span><span>'+c.ops+' ops</span><span>~'+fmtVolShort(Math.round(avgMonth))+'/m</span></div></div>';
|
||||
return '<div class="top-client-card" data-id="'+c.id+'" data-nome="'+esc(c.nome)+'"><span class="tc-rank">#'+c._rank+'</span><div class="tc-name" title="'+esc(c.nome)+'">'+esc(c.nome)+'</div><div class="tc-stats"><span class="tc-vol">'+fmtVolShort(c.vol)+'</span><span>'+c.ops+' ops</span><span>~'+fmtVolShort(Math.round(avgMonth))+'/m</span></div></div>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
var html = '';
|
||||
if (high.length) html += '<div class="top-clients-tier"><div class="tier-header"><span class="tier-dot tier-high"></span><span class="tier-label">Alto Volume</span><span class="tier-count">'+high.length+' clientes</span></div>'+buildCards(high)+'</div>';
|
||||
if (mid.length) html += '<div class="top-clients-tier"><div class="tier-header"><span class="tier-dot tier-mid"></span><span class="tier-label">Medio Volume</span><span class="tier-count">'+mid.length+' clientes</span></div>'+buildCards(mid)+'</div>';
|
||||
if (low.length) html += '<div class="top-clients-tier"><div class="tier-header"><span class="tier-dot tier-low"></span><span class="tier-label">Menor Volume</span><span class="tier-count">'+low.length+' clientes</span></div>'+buildCards(low)+'</div>';
|
||||
if (high.length) html += '<div class="top-clients-tier"><div class="tier-header"><span class="tier-dot tier-high"></span><span class="tier-label">Alto Volume</span><span class="tier-count">'+high.length+'</span></div>'+buildCards(high)+'</div>';
|
||||
if (mid.length) html += '<div class="top-clients-tier"><div class="tier-header"><span class="tier-dot tier-mid"></span><span class="tier-label">Medio Volume</span><span class="tier-count">'+mid.length+'</span></div>'+buildCards(mid)+'</div>';
|
||||
if (low.length) html += '<div class="top-clients-tier"><div class="tier-header"><span class="tier-dot tier-low"></span><span class="tier-label">Menor Volume</span><span class="tier-count">'+low.length+'</span></div>'+buildCards(low)+'</div>';
|
||||
el.innerHTML = html;
|
||||
el.addEventListener('click', function(e) {
|
||||
var card = e.target.closest('.top-client-card');
|
||||
if (card) selectClient(card.dataset.id, card.dataset.nome);
|
||||
});
|
||||
}
|
||||
// Server-side search
|
||||
document.getElementById('searchInput').addEventListener('input', function(e) {
|
||||
clearTimeout(_searchTimer);
|
||||
var val = e.target.value.trim().toLowerCase();
|
||||
var val = e.target.value.trim();
|
||||
_searchTimer = setTimeout(function() {
|
||||
var dd = document.getElementById('searchDropdown');
|
||||
if (val.length < 1) { dd.classList.remove('open'); return; }
|
||||
var matches = clientList.filter(function(c){return c.nome.toLowerCase().indexOf(val)!==-1;}).slice(0,15);
|
||||
if (!matches.length) { dd.innerHTML = '<div class="client-dropdown-item" style="color:var(--text-muted)">Nenhum resultado</div>'; dd.classList.add('open'); return; }
|
||||
dd.innerHTML = matches.map(function(c){return '<div class="client-dropdown-item" data-id="'+c.id+'" data-nome="'+c.nome.replace(/"/g,'"')+'">'+c.nome+'<span class="item-id">#'+c.id+'</span></div>';}).join('');
|
||||
dd.classList.add('open');
|
||||
}, 200);
|
||||
if (val.length < 2) { dd.classList.remove('open'); return; }
|
||||
fetch('/admin/api/clientes/search?q=' + encodeURIComponent(val)).then(function(r){return r.json();}).then(function(matches) {
|
||||
if (!matches.length) { dd.innerHTML = '<div class="client-dropdown-item" style="color:var(--text-muted)">Nenhum resultado</div>'; dd.classList.add('open'); return; }
|
||||
dd.innerHTML = matches.map(function(c){return '<div class="client-dropdown-item" data-id="'+c.id+'" data-nome="'+esc(c.nome)+'">'+esc(c.nome)+'<span class="item-id">#'+c.id+'</span></div>';}).join('');
|
||||
dd.classList.add('open');
|
||||
}).catch(function(){ dd.classList.remove('open'); });
|
||||
}, 300);
|
||||
});
|
||||
document.getElementById('searchDropdown').addEventListener('click', function(e) {
|
||||
var item = e.target.closest('.client-dropdown-item'); if (!item || !item.dataset.id) return;
|
||||
|
||||
Reference in New Issue
Block a user