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:
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user