feat(dashboard): reorganiza cards, remove indicadores antigos e adiciona 'Valor médio por fatura' junto aos demais; ajusta gráfico mensal para seguir padrão de design

This commit is contained in:
2025-08-09 16:06:20 -03:00
parent 291eec35a8
commit 7f659a0058
4 changed files with 593 additions and 109 deletions

View File

@@ -2,106 +2,254 @@
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1 style="display: flex; align-items: center; gap: 10px;">
<i class="fas fa-chart-line"></i> Dashboard de Faturas
</h1>
<form method="get" style="margin: 20px 0;">
<label for="cliente">Selecionar Cliente:</label>
<select name="cliente" id="cliente" onchange="this.form.submit()">
<option value="">Todos</option>
{% for c in clientes %}
<option value="{{ c }}" {% if c == cliente_atual %}selected{% endif %}>{{ c }}</option>
{% endfor %}
</select>
</form>
<!-- Cards -->
<div style="display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 30px;">
{% for indicador in indicadores %}
<div style="
flex: 1 1 220px;
background: #2563eb;
color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
">
<strong>{{ indicador.titulo }}</strong>
<div style="font-size: 1.6rem; font-weight: bold; margin-top: 10px;">
{{ indicador.valor }}
</div>
</div>
{% endfor %}
<div id="loading" class="loading-backdrop">
<div class="spinner"></div>
<div class="loading-msg">Carregando dados…</div>
</div>
<h2 style="margin-bottom: 20px;"><i class="fas fa-chart-bar"></i> Análise da Decisão do STF (RE 574.706 15/03/2017)</h2>
<div style="display: flex; flex-wrap: wrap; gap: 20px;">
<div style="flex: 1;">
<h4>% de Faturas com ICMS na Base PIS/COFINS</h4>
<canvas id="graficoICMS"></canvas>
<style>
/* ---- Combobox estilizado ---- */
.combo {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 12px 44px 12px 14px;
font-size: 14px;
line-height: 1.2;
color: #111827;
box-shadow: 0 6px 20px rgba(0,0,0,.06);
transition: box-shadow .2s ease, border-color .2s ease, transform .2s ease;
}
.combo:focus { outline: none; border-color: #2563eb; box-shadow: 0 8px 28px rgba(37,99,235,.18); }
.combo-wrap { position: relative; display: inline-flex; align-items: center; gap: 8px; }
.combo-wrap:after {
content: "▾"; position: absolute; right: 12px; pointer-events: none; color:#6b7280; font-size: 12px;
}
/* ---- Cards ---- */
.cards { display: grid; grid-template-columns: repeat(12, 1fr); gap: 18px; margin: 22px 0 32px; }
.card {
grid-column: span 12;
display: grid; grid-template-columns: 82px 1fr; align-items: start;
background: #1f2937; /* cinza escuro */
color: #f9fafb; /* texto claro */
border-radius: 18px; padding: 18px;
box-shadow: 0 12px 34px rgba(0,0,0,.08);
position: relative; overflow: hidden;
transition: transform .18s ease, box-shadow .18s ease;
animation: pop .35s ease both;
}
.card:hover { transform: translateY(-2px); box-shadow: 0 18px 44px rgba(0,0,0,.1); }
@keyframes pop { from{ transform: scale(.98); opacity:.0 } to{ transform: scale(1); opacity:1 } }
.card .icon {
width: 72px; height: 72px; border-radius: 16px;
display: grid; place-items: center; font-size: 38px; color: #fff;
box-shadow: inset 0 0 40px rgba(255,255,255,.2);
}
.icon.blue { background: linear-gradient(135deg, #2563eb, #3b82f6); }
.icon.green { background: linear-gradient(135deg, #059669, #10b981); }
.icon.amber { background: linear-gradient(135deg, #d97706, #f59e0b); }
.metrics { padding-left: 16px; }
.value { font-size: 30px; font-weight: 800; color: #f9fafb; text-align: right; }
.label { margin-top: 6px; font-size: 13px; color: #d1d5db; text-align: right; }
/* Responsivo */
@media (min-width: 640px) { .card { grid-column: span 6; } }
@media (min-width: 1024px){ .card { grid-column: span 4; } }
.loading-backdrop{
position:fixed; inset:0; z-index:9999;
background:rgba(17,24,39,.55); backdrop-filter: blur(2px);
display:flex; align-items:center; justify-content:center; gap:12px;
transition:opacity .25s ease; opacity:1; pointer-events:auto;
}
.loading-backdrop.hide{ opacity:0; pointer-events:none; }
.spinner{
width:40px; height:40px; border:4px solid rgba(255,255,255,.3);
border-top-color:#60a5fa; border-radius:50%; animation:spin 1s linear infinite;
}
@keyframes spin{ to{ transform:rotate(360deg) } }
.loading-msg{ color:#fff; font-weight:600; }
/* Card simples para gráficos */
.panel{
background:#1f2937; /* mesmo fundo dos cards */
color:#f9fafb;
border-radius:18px;
padding:16px 18px 22px;
box-shadow:0 12px 34px rgba(0,0,0,.08);
margin-top:10px;
}
.panel-title{
margin:0 0 10px 0;
font-weight:700;
display:flex;align-items:center;gap:10px;
}
</style>
<h1 style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
<i class="fas fa-chart-line"></i> Dashboard de Faturas
</h1>
<form method="get" action="/" style="margin: 6px 0 18px">
<div class="combo-wrap">
<label for="cliente" style="font-size:13px;color:#374151">Selecionar Cliente:</label>
<select class="combo" name="cliente" id="cliente">
<option value="">Todos</option>
{% for c in clientes %}
<option value="{{ c }}" {% if c == cliente_atual %}selected{% endif %}>{{ c }}</option>
{% endfor %}
</select>
</div>
<div style="flex: 1;">
<h4>Valor Médio de Tributos com ICMS</h4>
<canvas id="graficoValor"></canvas>
</form>
<script>
document.getElementById('cliente').addEventListener('change', function () {
const u = new URL(window.location);
if (this.value) u.searchParams.set('cliente', this.value);
else u.searchParams.delete('cliente');
u.pathname = "/"; // garante que fica na raiz
window.location = u.toString();
});
</script>
<!-- Cards -->
<div class="cards">
<!-- Total de Clientes -->
<div class="card">
<div class="icon" style="background: linear-gradient(135deg,#7c3aed,#a78bfa)"><i class="fas fa-users"></i></div>
<div class="metrics">
<div class="value">{{ '{:,}'.format(total_clientes or 0).replace(',', '.') }}</div>
<div class="label">Total de clientes</div>
</div>
</div>
<!-- Total de Faturas -->
<div class="card">
<div class="icon blue"><i class="fas fa-file-invoice"></i></div>
<div class="metrics">
<div class="value">{{ '{:,}'.format(total_faturas or 0).replace(',', '.') }}</div>
<div class="label">Total de faturas processadas</div>
</div>
</div>
<!-- Restituição Corrigida -->
<div class="card">
<div class="icon green"><i class="fas fa-hand-holding-usd"></i></div>
<div class="metrics">
<div class="value">R$ {{ '{:,.2f}'.format(valor_restituicao_corrigida or 0).replace(',', 'X').replace('.', ',').replace('X', '.') }}</div>
<div class="label">Restituição corrigida (PIS+COFINS sobre ICMS)</div>
</div>
</div>
<!-- % ICMS na Base -->
<div class="card">
<div class="icon amber"><i class="fas fa-percentage"></i></div>
<div class="metrics">
<div class="value">{{ '{:.1f}%'.format(percentual_icms_base or 0) }}</div>
<div class="label">% de faturas com ICMS na base do PIS/COFINS</div>
</div>
</div>
<!-- Valor médio por fatura com ICMS na base -->
<div class="card">
<div class="icon" style="background: linear-gradient(135deg,#ef4444,#f97316)">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="metrics">
<div class="value">R$ {{ '{:,.2f}'.format(valor_medio_com_icms or 0).replace(',', 'X').replace('.', ',').replace('X', '.') }}</div>
<div class="label">Valor médio (PIS+COFINS sobre ICMS) por fatura</div>
</div>
</div>
</div>
<!-- Evolução mensal (card) -->
<div class="panel">
<h2 class="panel-title">
<i class="fas fa-chart-line"></i>
Evolução mensal do valor passível de recuperação
</h2>
<canvas id="graficoEvolucao" style="max-height:360px"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2"></script>
<script>
const ctx1 = document.getElementById('graficoICMS').getContext('2d');
new Chart(ctx1, {
type: 'bar',
data: {
labels: ['Antes da Decisão', 'Depois da Decisão'],
datasets: [{
label: '% com ICMS na Base',
data: {{ [analise_stf.antes.percentual_com_icms, analise_stf.depois.percentual_com_icms] | tojson }},
backgroundColor: ['#f39c12', '#e74c3c']
}]
},
options: {
responsive: true,
plugins: {
legend: { display: true },
title: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: '%' }
}
}
}
});
const ctxE = document.getElementById('graficoEvolucao').getContext('2d');
const ctx2 = document.getElementById('graficoValor').getContext('2d');
new Chart(ctx2, {
type: 'bar',
data: {
labels: ['Antes da Decisão', 'Depois da Decisão'],
datasets: [{
label: 'Valor Médio de PIS/COFINS com ICMS',
data: {{ [analise_stf.antes.media_valor, analise_stf.depois.media_valor] | tojson }},
backgroundColor: ['#2980b9', '#27ae60']
}]
},
options: {
responsive: true,
plugins: {
legend: { display: true },
title: { display: false }
const evoLabels = {{ serie_mensal_labels | tojson }};
const evoValores = {{ serie_mensal_valores | tojson }};
new Chart(ctxE, {
type: 'line',
data: {
labels: evoLabels,
datasets: [{
label: 'Valor corrigido (R$)',
data: evoValores,
fill: false,
tension: 0.25,
borderWidth: 3,
pointRadius: 4,
pointHoverRadius: 5
}]
},
scales: {
y: {
beginAtZero: true,
title: { display: true, text: 'R$' }
options: {
responsive: true,
plugins: {
legend: {
labels: {
usePointStyle: true, // legenda com “linha”, não retângulo
pointStyle: 'line'
}
},
datalabels: {
align: 'top',
anchor: 'end',
color: '#e5e7eb',
font: { weight: 600, size: 11 },
formatter: (v) => 'R$ ' + Number(v).toLocaleString('pt-BR', {
minimumFractionDigits: 2, maximumFractionDigits: 2
})
},
tooltip: {
callbacks: {
label: (ctx) => {
const v = ctx.parsed.y ?? 0;
return 'R$ ' + Number(v).toLocaleString('pt-BR', {
minimumFractionDigits: 2, maximumFractionDigits: 2
});
}
}
}
},
scales: {
x: { grid: { display: false } }, // remove linhas do fundo
y: {
grid: { display: false },
ticks: { callback: v => 'R$ ' + Number(v).toLocaleString('pt-BR') }
}
}
}
}
});
},
plugins: [ChartDataLabels] // ativa o plugin de rótulos
});
// Mostra overlay ao iniciar; esconde quando tudo carregar
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('loading');
// garante visível até 'load'
el.classList.remove('hide');
});
window.addEventListener('load', () => {
const el = document.getElementById('loading');
el.classList.add('hide');
});
</script>
{% endblock %}

View File

@@ -13,6 +13,13 @@
</select>
</form>
<div style="margin-bottom: 20px;">
<a href="/export-excel{% if cliente_atual %}?cliente={{ cliente_atual }}{% endif %}" class="btn btn-primary">
📥 Baixar Relatório Corrigido (Excel)
</a>
</div>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #2563eb; color: white;">