portal_publico/templates/dashboard-contabil-relatorio.html

898 lines
44 KiB
HTML

{% load static %}
{% load contabil_extras %}
<!doctype html>
<html lang="pt-br">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard Contábil — {{ apuracao.nome_empresa }} — {{ apuracao.competencia|competencia }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
<style>
:root {
--roxo-escuro: #3d2178;
--roxo-mais-escuro: #281552;
--roxo-claro: #efe8ff;
--roxo-medio: #7a5bc0;
--dourado: #b4872a;
--dourado-escuro: #8f6a1f;
--dourado-claro: #f6ecd4;
--dourado-vivo: #d9ac48;
--texto: #211d2b;
--texto-muted: #6b6478;
--borda: #e6e1f0;
--bg-canvas: #f5f3fa;
--sucesso-bg: #dcf5e3;
--sucesso-texto: #1a7a3d;
--falha-bg: #fbe2e2;
--falha-texto: #b3261e;
--aviso-bg: #fdf0d8;
--aviso-texto: #9a6a0a;
--sombra: 0 1px 2px rgba(40, 21, 82, 0.04), 0 8px 24px -12px rgba(40, 21, 82, 0.18);
--sombra-hover: 0 4px 10px rgba(40, 21, 82, 0.06), 0 16px 36px -14px rgba(40, 21, 82, 0.26);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background:
radial-gradient(circle at 15% -10%, rgba(122, 91, 192, 0.14), transparent 42%),
radial-gradient(circle at 100% 0%, rgba(180, 135, 42, 0.10), transparent 38%),
var(--bg-canvas);
color: var(--texto);
font-family: "Inter", "Segoe UI", Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
h1, h2, .dcr-card__value, .dcr-tab { font-family: "Manrope", "Inter", sans-serif; }
/* ---- Animações de entrada — nunca fixar opacity:0 fora de @keyframes,
só via `animation ... both`, pra que @media print (que zera toda
`animation`) volte sozinho ao estado normal, sem precisar de reset
extra. Mesmo espírito do "animation, não transition, pra reiniciar a
partir de display:none" já documentado no Portal (CLAUDE.md,
"Animações"), embora este documento seja autocontido. */
@keyframes dcrFadeSlideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes dcrFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes dcrScaleIn {
from { opacity: 0; transform: scale(0.96); }
to { opacity: 1; transform: scale(1); }
}
@keyframes dcrGlow {
0%, 100% { opacity: 0.55; }
50% { opacity: 1; }
}
.dcr-page { max-width: 1140px; margin: 0 auto; padding: 0 24px 56px; }
/* ---- Cabeçalho ---- */
.dcr-header {
position: relative;
overflow: hidden;
background: linear-gradient(135deg, var(--roxo-escuro) 0%, var(--roxo-mais-escuro) 100%);
color: #fff;
padding: 30px 28px;
margin: 0 -24px 28px;
display: flex;
align-items: center;
gap: 22px;
flex-wrap: wrap;
border-bottom: 3px solid var(--dourado);
animation: dcrFadeIn 500ms ease both;
}
.dcr-header::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at 85% -20%, rgba(217, 172, 72, 0.35), transparent 55%);
pointer-events: none;
animation: dcrGlow 6s ease-in-out infinite;
}
.dcr-header__brand { display: flex; align-items: center; gap: 18px; position: relative; z-index: 1; }
.dcr-header img { height: 52px; width: auto; filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.25)); }
.dcr-header h1 { margin: 0; font-size: 1.4rem; font-weight: 800; letter-spacing: -0.01em; }
.dcr-header p { margin: 4px 0 0; color: var(--dourado-claro); font-size: 0.86rem; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dcr-status-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 10px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
background: rgba(255, 255, 255, 0.14);
border: 1px solid rgba(255, 255, 255, 0.22);
}
.dcr-status-pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.dcr-status-pill--concluida { color: #7CE7A0; }
.dcr-status-pill--revisao { color: #F5D583; }
.dcr-print-btn {
margin-left: auto;
position: relative;
z-index: 1;
background: linear-gradient(135deg, var(--dourado-vivo), var(--dourado));
color: var(--roxo-mais-escuro);
border: none;
border-radius: 8px;
padding: 11px 20px;
font-family: inherit;
font-size: 0.86rem;
font-weight: 700;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 6px 18px -6px rgba(217, 172, 72, 0.7);
transition: transform 160ms ease, box-shadow 160ms ease;
}
.dcr-print-btn:hover { transform: translateY(-2px); box-shadow: 0 10px 24px -6px rgba(217, 172, 72, 0.85); }
.dcr-print-btn:active { transform: translateY(0); }
/* Botão de exportar XLSX — dentro do próprio <h2> da seção (Balancete/DRE),
que já é flex, então margin-left:auto sozinho empurra pro fim da linha
(mesmo truque do .dcr-print-btn dentro do header). Mais discreto que o
botão de imprimir (outline, não preenchido) por ser uma ação
secundária por seção, não a ação principal do documento. */
.dcr-export-btn {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 999px;
border: 1.5px solid var(--dourado);
color: var(--roxo-escuro);
background: #fff;
font-size: 0.78rem;
font-weight: 700;
text-decoration: none;
transition: background 160ms ease, transform 160ms ease;
}
.dcr-export-btn:hover { background: var(--dourado-claro); transform: translateY(-1px); }
.dcr-export-btn:active { transform: translateY(0); }
/* ---- Abas ---- */
.dcr-tabs {
position: relative;
display: flex;
gap: 4px;
border-bottom: 2px solid var(--borda);
margin-bottom: 26px;
animation: dcrFadeSlideUp 460ms ease 80ms both;
}
.dcr-tab {
position: relative;
z-index: 1;
background: none;
border: none;
cursor: pointer;
padding: 12px 20px;
font-size: 0.92rem;
font-weight: 700;
color: var(--texto-muted);
transition: color 160ms ease;
}
.dcr-tab:hover { color: var(--roxo-escuro); }
.dcr-tab.is-active { color: var(--roxo-escuro); }
.dcr-tabs__indicator {
position: absolute;
bottom: -2px;
left: 0;
height: 3px;
border-radius: 3px 3px 0 0;
background: linear-gradient(90deg, var(--dourado), var(--dourado-vivo));
transition: transform 280ms cubic-bezier(0.65, 0, 0.35, 1), width 280ms cubic-bezier(0.65, 0, 0.35, 1);
}
.dcr-tab-panel { animation: dcrFadeSlideUp 420ms ease 140ms both; }
.dcr-secao { margin-bottom: 34px; }
.dcr-secao h2 {
font-size: 1.02rem;
font-weight: 700;
color: var(--roxo-escuro);
border-bottom: 2px solid var(--borda);
padding-bottom: 9px;
margin: 0 0 16px;
display: flex;
align-items: center;
gap: 8px;
}
.dcr-secao h2 svg { color: var(--dourado); flex-shrink: 0; }
/* ---- Cards de indicador ---- */
.dcr-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 22px;
}
.dcr-card {
position: relative;
background: #fff;
border: 1px solid var(--borda);
border-radius: 14px;
padding: 16px 18px;
box-shadow: var(--sombra);
transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
animation: dcrFadeSlideUp 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: calc(var(--i, 0) * 55ms + 60ms);
}
.dcr-card:hover {
transform: translateY(-3px);
box-shadow: var(--sombra-hover);
border-color: rgba(122, 91, 192, 0.35);
}
.dcr-card__icon {
width: 38px;
height: 38px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
background: var(--roxo-claro);
color: var(--roxo-escuro);
}
.dcr-card--dourado .dcr-card__icon { background: var(--dourado-claro); color: var(--dourado-escuro); }
.dcr-card__label { font-size: 0.74rem; color: var(--texto-muted); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; }
.dcr-card__value {
font-size: 1.55rem;
font-weight: 800;
color: var(--roxo-escuro);
margin-top: 4px;
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
.dcr-card__value.dcr-valor-positivo { color: var(--sucesso-texto); }
.dcr-card__value.dcr-valor-negativo { color: var(--falha-texto); }
.dcr-card__nota { font-size: 0.7rem; color: var(--texto-muted); margin-top: 6px; line-height: 1.35; }
/* ---- Gráfico ---- */
.dcr-chart-wrap {
background: #fff;
border: 1px solid var(--borda);
border-radius: 14px;
padding: 20px;
box-shadow: var(--sombra);
animation: dcrFadeSlideUp 460ms ease 120ms both;
}
/* ---- Tabelas ---- */
.dcr-tabela-wrap {
background: #fff;
border: 1px solid var(--borda);
border-radius: 14px;
overflow: hidden;
box-shadow: var(--sombra);
animation: dcrScaleIn 380ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
table.dcr-tabela { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
table.dcr-tabela th, table.dcr-tabela td { padding: 9px 14px; border-bottom: 1px solid var(--borda); text-align: left; }
table.dcr-tabela th {
background: linear-gradient(180deg, var(--roxo-claro), #f4f0ff);
color: var(--roxo-escuro);
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
position: sticky;
top: 0;
}
table.dcr-tabela td.dcr-valor { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
table.dcr-tabela tbody tr { transition: background 120ms ease; }
table.dcr-tabela tbody tr:not(.dcr-linha-total):hover { background: #faf9ff; }
table.dcr-tabela tbody tr:nth-child(even):not(.dcr-linha-total) { background: rgba(122, 91, 192, 0.025); }
tr.dcr-linha-total td { font-weight: 700; background: var(--dourado-claro); }
.dcr-row-in { animation: dcrFadeIn 220ms ease; }
.dcr-desc-cell { display: flex; align-items: center; gap: 7px; }
.dcr-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
border-radius: 5px;
color: var(--texto-muted);
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.dcr-toggle:hover { color: var(--roxo-escuro); background: var(--roxo-claro); }
.dcr-toggle svg { transition: transform 160ms cubic-bezier(0.65, 0, 0.35, 1); }
.dcr-toggle.is-expanded svg { transform: rotate(90deg); }
.dcr-toggle-spacer { display: inline-block; width: 18px; height: 18px; flex-shrink: 0; }
/* ---- Observações ---- */
.dcr-obs-lista { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.dcr-obs-item {
display: flex;
gap: 12px;
background: #fff;
border: 1px solid var(--borda);
border-radius: 12px;
padding: 13px 16px;
box-shadow: var(--sombra);
animation: dcrFadeSlideUp 380ms ease both;
animation-delay: calc(var(--i, 0) * 45ms);
transition: box-shadow 160ms ease, transform 160ms ease;
}
.dcr-obs-item:hover { transform: translateX(2px); box-shadow: var(--sombra-hover); }
.dcr-obs-item__icone {
flex-shrink: 0;
width: 30px;
height: 30px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: var(--dourado-claro);
color: var(--dourado-escuro);
}
.dcr-obs-item__titulo { font-weight: 700; font-size: 0.85rem; color: var(--roxo-escuro); }
.dcr-obs-item__texto { font-size: 0.85rem; margin-top: 3px; white-space: pre-wrap; color: var(--texto); }
.dcr-vazio { color: var(--texto-muted); font-size: 0.85rem; font-style: italic; }
.dcr-footer { color: var(--texto-muted); font-size: 0.76rem; text-align: center; margin-top: 28px; }
@media (max-width: 640px) {
.dcr-header { padding: 22px 20px; }
.dcr-tabs { overflow-x: auto; }
}
@media print {
* { animation: none !important; transition: none !important; }
body { background: #fff; }
.dcr-print-btn { display: none; }
.dcr-export-btn { display: none; }
.dcr-header { margin: 0 0 24px; }
.dcr-tabs { display: none; }
.dcr-tab-panel { display: block !important; }
.dcr-tab-panel:not(:last-child) { page-break-after: always; }
.dcr-tabela tbody tr[hidden] { display: table-row !important; }
.dcr-toggle { display: none; }
.dcr-card, .dcr-tabela-wrap, .dcr-obs-item { box-shadow: none !important; }
}
</style>
</head>
<body>
<div class="dcr-page">
<header class="dcr-header">
<div class="dcr-header__brand">
<img src="{% static 'img/logo-branco.png' %}" alt="De Paula Contadores">
<div>
<h1>Dashboard Contábil — {{ apuracao.nome_empresa }}</h1>
<p>
CNPJ {{ apuracao.cnpj|default:"não informado" }} · Competência {{ apuracao.competencia|competencia }}
<span class="dcr-status-pill dcr-status-pill--{{ apuracao.status }}">{{ apuracao.get_status_display }}</span>
</p>
</div>
</div>
<button type="button" class="dcr-print-btn" onclick="window.print()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
Imprimir
</button>
</header>
<nav class="dcr-tabs">
<span class="dcr-tabs__indicator" id="dcr-tabs-indicator"></span>
<button type="button" class="dcr-tab is-active" data-dcr-tab="balancete">Balancete</button>
<button type="button" class="dcr-tab" data-dcr-tab="dre">D.R.E.</button>
<button type="button" class="dcr-tab" data-dcr-tab="indicadores">Indicadores</button>
</nav>
<div class="dcr-tab-panel" data-dcr-panel="balancete">
<div class="dcr-secao">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
Balancete
<a class="dcr-export-btn" href="/api/contabil-apuracoes/{{ apuracao.id }}/exportar-xlsx/?parte=balancete">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
Exportar XLSX
</a>
</h2>
<div class="dcr-tabela-wrap">
<table class="dcr-tabela">
<thead>
<tr>
<th>Conta</th>
<th>Descrição</th>
<th class="dcr-valor">Saldo Anterior</th>
<th class="dcr-valor">Débito</th>
<th class="dcr-valor">Crédito</th>
<th class="dcr-valor">Saldo Atual</th>
</tr>
</thead>
<tbody id="dcr-balancete-body">
{% for item in contas %}
<tr{% if item.conta.tipo == "S" %} class="dcr-linha-total"{% endif %} data-dcr-nivel="{{ item.nivel }}" data-dcr-id="conta-{{ item.conta.id }}" data-dcr-tem-filhos="{% if item.tem_filhos %}1{% else %}0{% endif %}">
<td>{{ item.conta.codigo }}</td>
<td style="padding-left: {{ item.nivel_px }}px">
<span class="dcr-desc-cell">
{% if item.tem_filhos %}
<button type="button" class="dcr-toggle is-expanded" data-dcr-toggle aria-label="Recolher/expandir">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
{% else %}
<span class="dcr-toggle-spacer"></span>
{% endif %}
<span>{{ item.conta.descricao }}</span>
</span>
</td>
<td class="dcr-valor">{{ item.conta.saldo_anterior|moeda }}</td>
<td class="dcr-valor">{{ item.conta.debito|moeda }}</td>
<td class="dcr-valor">{{ item.conta.credito|moeda }}</td>
<td class="dcr-valor">{{ item.conta.saldo_atual|moeda }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="dcr-secao">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/></svg>
Observações do Balancete
</h2>
{% if observacoes_contas %}
<ul class="dcr-obs-lista">
{% for conta in observacoes_contas %}
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg></div>
<div>
<div class="dcr-obs-item__titulo">{{ conta.codigo }} — {{ conta.descricao }}</div>
<div class="dcr-obs-item__texto">{{ conta.observacao }}</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="dcr-vazio">Nenhuma observação registrada no Balancete.</p>
{% endif %}
</div>
</div>
<div class="dcr-tab-panel" data-dcr-panel="dre" hidden>
<div class="dcr-secao">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5a2 2 0 0 1 2-2h8l6 6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/><path d="M14 3v6h6M9 13h6M9 17h6"/></svg>
D.R.E. — Demonstração do Resultado do Exercício
<a class="dcr-export-btn" href="/api/contabil-apuracoes/{{ apuracao.id }}/exportar-xlsx/?parte=dre">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
Exportar XLSX
</a>
</h2>
<div class="dcr-tabela-wrap">
<table class="dcr-tabela">
<thead><tr><th>Descrição</th><th class="dcr-valor">Valor</th></tr></thead>
<tbody id="dcr-dre-body">
{% for item in linhas_dre %}
<tr{% if item.linha.totalizador %} class="dcr-linha-total"{% endif %} data-dcr-nivel="{{ item.nivel }}" data-dcr-id="linha-{{ item.linha.id }}" data-dcr-tem-filhos="{% if item.tem_filhos %}1{% else %}0{% endif %}">
<td style="padding-left: {{ item.nivel_px|add:12 }}px">
<span class="dcr-desc-cell">
{% if item.tem_filhos %}
<button type="button" class="dcr-toggle is-expanded" data-dcr-toggle aria-label="Recolher/expandir">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
{% else %}
<span class="dcr-toggle-spacer"></span>
{% endif %}
<span>{{ item.linha.descricao }}</span>
</span>
</td>
<td class="dcr-valor">{{ item.linha.valor|moeda }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="dcr-secao">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/></svg>
Observações da D.R.E.
</h2>
{% if observacoes_dre %}
<ul class="dcr-obs-lista">
{% for linha in observacoes_dre %}
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5a2 2 0 0 1 2-2h8l6 6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/><path d="M14 3v6h6"/></svg></div>
<div>
<div class="dcr-obs-item__titulo">{{ linha.descricao }}</div>
<div class="dcr-obs-item__texto">{{ linha.observacao }}</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="dcr-vazio">Nenhuma observação registrada na D.R.E.</p>
{% endif %}
</div>
</div>
<div class="dcr-tab-panel" data-dcr-panel="indicadores" hidden>
<div class="dcr-secao">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg>
Indicadores de Resultado
</h2>
<div class="dcr-cards">
<div class="dcr-card" style="--i:0">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><rect x="7" y="12" width="3" height="6"/><rect x="12" y="8" width="3" height="10"/><rect x="17" y="5" width="3" height="13"/></svg></div>
<div class="dcr-card__label">ROA</div>
<div class="dcr-card__value" data-final="{{ indicadores.roa|percentual }}" data-count="{{ indicadores.roa|numero_bruto }}" data-format="pct" data-color-rule="sign">{{ indicadores.roa|percentual }}</div>
</div>
<div class="dcr-card" style="--i:1">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg></div>
<div class="dcr-card__label">ROE</div>
<div class="dcr-card__value" data-final="{{ indicadores.roe|percentual }}" data-count="{{ indicadores.roe|numero_bruto }}" data-format="pct" data-color-rule="sign">{{ indicadores.roe|percentual }}</div>
</div>
<div class="dcr-card" style="--i:2">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 1 0 10 10"/><path d="M12 12 20 4"/><path d="M12 2v4"/></svg></div>
<div class="dcr-card__label">Kanitz</div>
<div class="dcr-card__value" data-final="{{ indicadores.kanitz|indice }}" data-count="{{ indicadores.kanitz|numero_bruto }}" data-format="idx">{{ indicadores.kanitz|indice }}</div>
<div class="dcr-card__nota">Estimativa — fórmula padrão, não calibrada</div>
</div>
<div class="dcr-card dcr-card--dourado" style="--i:3">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 0 1 5 0c0 1.5-2.5 2-2.5 4M12 16.5v.01"/></svg></div>
<div class="dcr-card__label">EBIT</div>
<div class="dcr-card__value" data-final="{{ indicadores.ebit|moeda }}" data-count="{{ indicadores.ebit|numero_bruto }}" data-format="moeda" data-color-rule="sign">{{ indicadores.ebit|moeda }}</div>
</div>
<div class="dcr-card dcr-card--dourado" style="--i:4">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 11v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6"/></svg></div>
<div class="dcr-card__label">EBITDA</div>
<div class="dcr-card__value" data-final="{{ indicadores.ebitda|moeda }}" data-count="{{ indicadores.ebitda|numero_bruto }}" data-format="moeda" data-color-rule="sign">{{ indicadores.ebitda|moeda }}</div>
{% if indicadores.ebitda is None %}<div class="dcr-card__nota">Sem apuração anterior desta empresa</div>{% endif %}
</div>
</div>
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11Z"/></svg>
Indicadores de Liquidez e Endividamento
</h2>
<div class="dcr-cards">
<div class="dcr-card" style="--i:5">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l3 8 4-16 3 8h4"/></svg></div>
<div class="dcr-card__label">Liquidez Corrente</div>
<div class="dcr-card__value" data-final="{{ indicadores.liquidez_corrente|indice }}" data-count="{{ indicadores.liquidez_corrente|numero_bruto }}" data-format="idx" data-color-rule="liquidez">{{ indicadores.liquidez_corrente|indice }}</div>
</div>
<div class="dcr-card" style="--i:6">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11Z"/><path d="M9 13a3 3 0 0 0 3 3"/></svg></div>
<div class="dcr-card__label">Liquidez Seca</div>
<div class="dcr-card__value" data-final="{{ indicadores.liquidez_seca|indice }}" data-count="{{ indicadores.liquidez_seca|numero_bruto }}" data-format="idx" data-color-rule="liquidez">{{ indicadores.liquidez_seca|indice }}</div>
</div>
<div class="dcr-card" style="--i:7">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
<div class="dcr-card__label">Liquidez Geral</div>
<div class="dcr-card__value" data-final="{{ indicadores.liquidez_geral|indice }}" data-count="{{ indicadores.liquidez_geral|numero_bruto }}" data-format="idx" data-color-rule="liquidez">{{ indicadores.liquidez_geral|indice }}</div>
<div class="dcr-card__nota">Aproximado — ver observação no relatório técnico</div>
</div>
<div class="dcr-card dcr-card--dourado" style="--i:8">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3"/><path d="M3 8h18v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/><path d="M3 12h18"/></svg></div>
<div class="dcr-card__label">Composição do Endividamento</div>
<div class="dcr-card__value" data-final="{{ indicadores.composicao_endividamento|percentual }}" data-count="{{ indicadores.composicao_endividamento|numero_bruto }}" data-format="pct">{{ indicadores.composicao_endividamento|percentual }}</div>
</div>
<div class="dcr-card dcr-card--dourado" style="--i:9">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v.5"/><path d="M14 9V5a2 2 0 0 1 2-2h0a2 2 0 0 1 2 2v4"/><path d="M6 9h12l1 11H5Z"/></svg></div>
<div class="dcr-card__label">Grau de Endividamento</div>
<div class="dcr-card__value" data-final="{{ indicadores.grau_endividamento|percentual }}" data-count="{{ indicadores.grau_endividamento|numero_bruto }}" data-format="pct">{{ indicadores.grau_endividamento|percentual }}</div>
</div>
<div class="dcr-card dcr-card--dourado" style="--i:10">
<div class="dcr-card__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V9l9-6 9 6v12"/><path d="M9 21v-8h6v8M3 9h18"/></svg></div>
<div class="dcr-card__label">IPL</div>
<div class="dcr-card__value" data-final="{{ indicadores.ipl|percentual }}" data-count="{{ indicadores.ipl|numero_bruto }}" data-format="pct">{{ indicadores.ipl|percentual }}</div>
</div>
</div>
</div>
<div class="dcr-secao">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg>
Evolução do Resultado Líquido
</h2>
<div class="dcr-chart-wrap">
{% if evolucao|length > 1 %}
<canvas id="dcr-grafico-evolucao" height="80"></canvas>
{% else %}
<p class="dcr-vazio">Ainda não há histórico suficiente desta empresa no Portal para um gráfico de evolução.</p>
{% endif %}
</div>
</div>
<div class="dcr-secao">
<h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/></svg>
Todas as Observações da Análise
</h2>
{% if observacoes_contas or observacoes_dre or achados_com_observacao %}
<ul class="dcr-obs-lista">
{% for conta in observacoes_contas %}
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg></div>
<div>
<div class="dcr-obs-item__titulo">Balancete — {{ conta.codigo }} {{ conta.descricao }}</div>
<div class="dcr-obs-item__texto">{{ conta.observacao }}</div>
</div>
</li>
{% endfor %}
{% for linha in observacoes_dre %}
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5a2 2 0 0 1 2-2h8l6 6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/><path d="M14 3v6h6"/></svg></div>
<div>
<div class="dcr-obs-item__titulo">D.R.E. — {{ linha.descricao }}</div>
<div class="dcr-obs-item__texto">{{ linha.observacao }}</div>
</div>
</li>
{% endfor %}
{% for achado in achados_com_observacao %}
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><path d="M12 9v4M12 17h.01"/></svg></div>
<div>
<div class="dcr-obs-item__titulo">Auditoria — {{ achado.titulo }}{% if achado.tratado_por %} — {{ achado.tratado_por.nome }}{% endif %}</div>
<div class="dcr-obs-item__texto">{{ achado.observacao_contador }}</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="dcr-vazio">Nenhuma observação registrada nesta análise.</p>
{% endif %}
</div>
</div>
<p class="dcr-footer">Gerado pelo Portal Interno da De Paula em {{ gerado_em|date:"d/m/Y H:i" }}.</p>
</div>
{{ evolucao|json_script:"dcr-evolucao-data" }}
<script>
(function () {
// ---- Abas (Balancete / D.R.E. / Indicadores), com indicador deslizante ----
var tabs = Array.prototype.slice.call(document.querySelectorAll("[data-dcr-tab]"));
var paineis = document.querySelectorAll("[data-dcr-panel]");
var indicador = document.getElementById("dcr-tabs-indicator");
var graficoInicializado = false;
function posicionaIndicador(tab) {
if (!indicador || !tab) return;
indicador.style.width = tab.offsetWidth + "px";
indicador.style.transform = "translateX(" + tab.offsetLeft + "px)";
}
function ativaTab(tab) {
var alvo = tab.getAttribute("data-dcr-tab");
tabs.forEach(function (t) { t.classList.toggle("is-active", t === tab); });
paineis.forEach(function (p) { p.hidden = p.getAttribute("data-dcr-panel") !== alvo; });
posicionaIndicador(tab);
// O <canvas> do gráfico e os cards de indicador começam dentro de
// uma aba escondida (Balancete é a aba inicial) — Chart.js não
// desenha direito num canvas com largura/altura zero, e a contagem
// animada dos cards não faz sentido rodar enquanto está invisível,
// então os dois só disparam na primeira vez que a aba "Indicadores"
// é aberta de fato.
if (alvo === "indicadores" && !graficoInicializado) {
graficoInicializado = true;
inicializaGrafico();
pidDcrAnimaContadores();
}
}
tabs.forEach(function (tab) {
tab.addEventListener("click", function () { ativaTab(tab); });
});
window.addEventListener("resize", function () {
var ativa = tabs.filter(function (t) { return t.classList.contains("is-active"); })[0];
posicionaIndicador(ativa);
});
// Posição inicial só depois das fontes carregarem/layout assentar,
// senão `offsetLeft`/`offsetWidth` do primeiro paint saem errados.
window.addEventListener("load", function () { posicionaIndicador(tabs[0]); });
posicionaIndicador(tabs[0]);
// ---- Balancete/D.R.E.: árvore recolhível ----
// Mesmo algoritmo de renderContas()/renderDre() em dashboard-contabil.js
// (pilha de níveis recolhidos, "tem filhos" = próxima linha com nível
// maior) — mas aqui todas as linhas já estão prontas no HTML (o nível
// e "tem filhos" vieram calculados do servidor, ver
// _contabil_arvore_contexto em views.py), então alternar uma linha só
// precisa mostrar/esconder as `<tr>` já existentes, sem reconstruir
// nada. Linhas que passam de escondidas pra visíveis ganham um fade
// rápido (`.dcr-row-in`) — só elas, não a tabela inteira, pra não
// gerar flicker num clique que só afeta um grupo pequeno.
function pidDcrArvore(tbodyId) {
var tbody = document.getElementById(tbodyId);
if (!tbody) return;
var linhas = Array.prototype.slice.call(tbody.querySelectorAll("tr"));
var colapsadas = {};
function atualiza() {
var pilha = [];
linhas.forEach(function (tr) {
var eraOculta = tr.hidden;
var nivel = Number(tr.getAttribute("data-dcr-nivel"));
while (pilha.length && pilha[pilha.length - 1] >= nivel) pilha.pop();
var oculta = pilha.length > 0;
var id = tr.getAttribute("data-dcr-id");
var temFilhos = tr.getAttribute("data-dcr-tem-filhos") === "1";
if (temFilhos && colapsadas[id]) pilha.push(nivel);
tr.hidden = oculta;
if (eraOculta && !oculta) {
tr.classList.remove("dcr-row-in");
void tr.offsetWidth; // força reflow pra reiniciar a animação
tr.classList.add("dcr-row-in");
}
});
}
tbody.addEventListener("click", function (event) {
var btn = event.target.closest("[data-dcr-toggle]");
if (!btn) return;
var tr = btn.closest("tr");
var id = tr.getAttribute("data-dcr-id");
colapsadas[id] = !colapsadas[id];
btn.classList.toggle("is-expanded", !colapsadas[id]);
atualiza();
});
}
pidDcrArvore("dcr-balancete-body");
pidDcrArvore("dcr-dre-body");
// ---- Cards: contagem animada + cor por sinal/liquidez ----
// O texto final exibido é sempre `data-final` (a string já formatada
// pelos filtros `moeda`/`percentual`/`indice` no servidor) — a
// animação só interpola os quadros intermediários com Intl nativo do
// navegador; nunca recalcula o valor de verdade, só decora a
// transição até ele. `finalizadores` guarda uma função por card pra
// pular direto pro valor final — usado no `beforeprint`, pra nunca
// imprimir um card a meio de uma animação.
var finalizadores = [];
function formataIntermediario(valor, formato) {
if (formato === "pct") {
return (valor * 100).toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + "%";
}
if (formato === "moeda") {
return valor.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
}
return valor.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function aplicaCor(el, valor) {
var regra = el.getAttribute("data-color-rule");
if (!regra || isNaN(valor)) return;
el.classList.remove("dcr-valor-positivo", "dcr-valor-negativo");
if (regra === "sign") {
if (valor > 0) el.classList.add("dcr-valor-positivo");
else if (valor < 0) el.classList.add("dcr-valor-negativo");
} else if (regra === "liquidez") {
el.classList.add(valor >= 1 ? "dcr-valor-positivo" : "dcr-valor-negativo");
}
}
// `instantaneo` pula a animação e escreve o valor final direto — usado
// no fallback de impressão (ver `beforeprint` mais abaixo), quando a
// aba "Indicadores" nunca foi aberta na tela: sem isso, a impressão
// poderia capturar os cards no meio da contagem (RAF não termina a
// tempo do `window.print()` seguir em frente).
function pidDcrAnimaContadores(instantaneo) {
document.querySelectorAll(".dcr-card__value[data-count]").forEach(function (el) {
var bruto = el.getAttribute("data-count");
var final = el.getAttribute("data-final");
var formato = el.getAttribute("data-format");
if (bruto === "" || bruto === null) return; // indisponível ("—") — nada a animar
var alvo = parseFloat(bruto);
aplicaCor(el, alvo);
if (instantaneo) {
el.textContent = final;
return;
}
var inicio = null;
var duracao = 800;
function passo(agora) {
if (inicio === null) inicio = agora;
var progresso = Math.min(1, (agora - inicio) / duracao);
var suavizado = 1 - Math.pow(1 - progresso, 3); // easeOutCubic
el.textContent = formataIntermediario(alvo * suavizado, formato);
if (progresso < 1) {
requestAnimationFrame(passo);
} else {
el.textContent = final;
}
}
finalizadores.push(function () { el.textContent = final; });
requestAnimationFrame(passo);
});
}
// ---- Gráfico de evolução do Resultado Líquido ----
function inicializaGrafico() {
var dados = JSON.parse(document.getElementById("dcr-evolucao-data").textContent);
var canvas = document.getElementById("dcr-grafico-evolucao");
if (!canvas || dados.length < 2 || typeof Chart === "undefined") return;
var ctx = canvas.getContext("2d");
var gradiente = ctx.createLinearGradient(0, 0, 0, canvas.height || 260);
gradiente.addColorStop(0, "rgba(61, 33, 120, 0.28)");
gradiente.addColorStop(1, "rgba(61, 33, 120, 0.02)");
Chart.defaults.font.family = "Inter, sans-serif";
new Chart(ctx, {
type: "line",
data: {
labels: dados.map(function (p) { return p.competencia; }),
datasets: [{
label: "Resultado Líquido (R$)",
data: dados.map(function (p) { return parseFloat(p.resultado_liquido); }),
borderColor: "#3d2178",
backgroundColor: gradiente,
pointBackgroundColor: "#b4872a",
pointBorderColor: "#fff",
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6,
borderWidth: 2.5,
tension: 0.35,
fill: true,
}],
},
options: {
responsive: true,
animation: { duration: 900, easing: "easeOutCubic" },
interaction: { intersect: false, mode: "index" },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: "#3d2178",
titleFont: { weight: "700" },
padding: 10,
cornerRadius: 8,
callbacks: {
label: function (item) { return "R$ " + item.parsed.y.toLocaleString("pt-BR", { minimumFractionDigits: 2 }); },
},
},
},
scales: {
x: { grid: { display: false } },
y: {
grid: { color: "rgba(228, 224, 236, 0.7)" },
ticks: { callback: function (v) { return "R$ " + v.toLocaleString("pt-BR"); } },
},
},
},
});
}
// Impressão mostra as 3 abas seguidas (ver @media print). Dois casos:
// (1) "Indicadores" nunca foi clicada na tela — gráfico e cards ainda
// não existem, então monta os dois agora, com os cards já no valor
// final (sem animação, ver `pidDcrAnimaContadores(true)` acima); (2)
// já foi clicada e a contagem pode estar em andamento — só finaliza
// os valores já em animação. Um único listener, pra nunca competir
// com o outro caso.
window.addEventListener("beforeprint", function () {
if (!graficoInicializado) {
graficoInicializado = true;
inicializaGrafico();
pidDcrAnimaContadores(true);
} else {
finalizadores.forEach(function (fn) { fn(); });
}
});
})();
</script>
</body>
</html>