feat: top 20 clientes por volume com cards clicaveis agrupados por faixa
- fetchClientList agora retorna volume USD, ops, meses ativos e ultima op - Top 20 clientes exibidos como cards clicaveis na tela inicial - Agrupados em 3 faixas: Alto Volume, Medio Volume, Menor Volume - Cards mostram volume total, ops e media mensal - Click no card carrega o dashboard 360 do cliente Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -130,10 +130,36 @@ function buildAdminClienteHTML(user) {
|
||||
.client-selected-badge .badge-clear:hover { border-color: var(--red); color: var(--red); }
|
||||
|
||||
/* Empty State */
|
||||
.empty-state { text-align: center; padding: 80px 20px; color: var(--text-muted); }
|
||||
.empty-state .empty-icon { font-size: 64px; margin-bottom: 16px; opacity: 0.3; }
|
||||
.empty-state h2 { font-size: 20px; font-weight: 700; margin-bottom: 8px; color: var(--text-secondary); }
|
||||
.empty-state p { font-size: 14px; }
|
||||
.empty-state { text-align: center; padding: 40px 20px 20px; color: var(--text-muted); }
|
||||
.empty-state .empty-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.3; }
|
||||
.empty-state h2 { font-size: 20px; font-weight: 700; margin-bottom: 6px; color: var(--text-secondary); }
|
||||
.empty-state p { font-size: 14px; margin-bottom: 0; }
|
||||
|
||||
/* Top Clients Grid */
|
||||
.top-clients-section { max-width: 1200px; margin: 0 auto; padding: 0 16px; }
|
||||
.top-clients-tier { margin-bottom: 20px; }
|
||||
.tier-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; padding: 0 4px; }
|
||||
.tier-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||||
.tier-dot.tier-high { background: var(--green, #1E8E3E); }
|
||||
.tier-dot.tier-mid { background: var(--blue, #58A6FF); }
|
||||
.tier-dot.tier-low { background: var(--orange, #F0883E); }
|
||||
.tier-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); }
|
||||
.tier-count { font-size: 11px; color: var(--text-muted); margin-left: auto; }
|
||||
.top-clients-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
|
||||
.top-client-card {
|
||||
padding: 14px 16px; border-radius: 10px; cursor: pointer; transition: all 0.15s;
|
||||
background: var(--card); border: 1px solid var(--border); position: relative; overflow: hidden;
|
||||
}
|
||||
.top-client-card:hover { border-color: var(--tc-accent); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||
[data-theme="dark"] .top-client-card { background: rgba(255,255,255,0.02); }
|
||||
[data-theme="dark"] .top-client-card:hover { background: rgba(0,255,136,0.04); border-color: #00FF88; box-shadow: 0 4px 16px rgba(0,255,136,0.08); }
|
||||
.top-client-card .tc-rank { position: absolute; top: 8px; right: 10px; font-size: 10px; font-weight: 800; color: var(--text-muted); opacity: 0.5; }
|
||||
.top-client-card .tc-name { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 24px; }
|
||||
.top-client-card .tc-stats { display: flex; gap: 12px; font-size: 11px; color: var(--text-secondary); }
|
||||
.top-client-card .tc-stats span { display: flex; align-items: center; gap: 3px; }
|
||||
.top-client-card .tc-vol { font-weight: 700; color: var(--tc-accent); }
|
||||
[data-theme="dark"] .top-client-card .tc-name { font-family: 'SF Mono','Fira Code','Consolas',monospace; }
|
||||
@media (max-width: 480px) { .top-clients-grid { grid-template-columns: 1fr 1fr; gap: 8px; } .top-client-card { padding: 10px 12px; } }
|
||||
|
||||
/* === Profile Card === */
|
||||
.profile-card { border-radius: 16px; padding: 24px; margin-bottom: 24px; display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }
|
||||
@@ -329,11 +355,14 @@ ${buildHeader({ role: role, userName: user.nome, activePage: 'cliente' })}
|
||||
<button class="badge-clear" onclick="clearClient()">Limpar</button>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="empty-state" id="emptyState">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<h2>Selecione um cliente</h2>
|
||||
<p>Use a busca acima para encontrar um cliente e visualizar sua analise 360 completa.</p>
|
||||
<!-- Empty State + Top Clients -->
|
||||
<div id="emptyState">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<h2>Selecione um cliente</h2>
|
||||
<p>Busque acima ou clique em um dos top clientes abaixo</p>
|
||||
</div>
|
||||
<div class="top-clients-section" id="topClientsSection"></div>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
@@ -725,14 +754,45 @@ function renderNetting(data) {
|
||||
|
||||
// === Client Search ===
|
||||
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 loadClients() {
|
||||
fetch('/admin/api/clientes').then(function(r){return r.json();}).then(function(data) {
|
||||
clientList = data;
|
||||
renderTopClients(data.slice(0, 20));
|
||||
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); }
|
||||
}).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;
|
||||
top.forEach(function(c) {
|
||||
var ratio = c.vol / maxVol;
|
||||
if (ratio >= 0.3) high.push(c);
|
||||
else if (ratio >= 0.05) mid.push(c);
|
||||
else low.push(c);
|
||||
});
|
||||
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>';
|
||||
}).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>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
document.getElementById('searchInput').addEventListener('input', function(e) {
|
||||
clearTimeout(_searchTimer);
|
||||
var val = e.target.value.trim().toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user