portal_publico/templates/dashboard-contabil-relatorio.html

1208 lines
55 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>Relatório 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&family=JetBrains+Mono:wght@500;600&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;
}
/* Flip card — pedido explícito do usuário: passar o mouse revela a
descrição/fórmula do indicador (cadastradas em "Gerenciar
Indicadores", ver IndicadorContabilDefinicao.descricao/.formula), sem
precisar de nenhum clique. `.dcr-card` é só a "cena" 3D (perspective +
min-height, já que as duas faces são absolutamente posicionadas e não
contribuem mais pra altura do card); `.dcr-card-inner` é quem
realmente gira (`rotateY`); cada face (frente/verso) carrega o
fundo/borda/sombra que antes viviam direto em `.dcr-card`.
`backface-visibility:hidden` garante que só uma face fica visível de
cada vez. Em `@media print`, `transition`/`animation` são zeradas
globalmente (ver bloco mais abaixo) — o card sempre imprime a frente,
já que impressão não tem "hover" ativo. */
.dcr-card {
position: relative;
perspective: 1200px;
min-height: 172px;
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); }
.dcr-card-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 480ms cubic-bezier(0.4, 0.2, 0.2, 1);
transform-style: preserve-3d;
}
.dcr-card:hover .dcr-card-inner { transform: rotateY(180deg); }
.dcr-card-face {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
background: #fff;
border: 1px solid var(--borda);
border-radius: 14px;
padding: 16px 18px;
box-shadow: var(--sombra);
backface-visibility: hidden;
transition: box-shadow 200ms ease, border-color 200ms ease;
}
.dcr-card:hover .dcr-card-face { box-shadow: var(--sombra-hover); border-color: rgba(122, 91, 192, 0.35); }
.dcr-card-face--back { transform: rotateY(180deg); overflow-y: auto; text-align: center; }
.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.3rem;
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; }
/* ---- Verso do flip card: descrição + fórmula ---- */
.dcr-card-face__titulo {
font-size: 0.74rem;
font-weight: 700;
color: var(--roxo-escuro);
margin: 0 0 6px;
}
.dcr-card-face__descricao {
font-size: 0.7rem;
color: var(--texto);
line-height: 1.4;
margin: 0 0 10px;
}
.dcr-card-face__formula-label {
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--texto-muted);
font-weight: 600;
margin: 0 0 4px;
}
.dcr-card-face__formula {
display: block;
font-family: "JetBrains Mono", "Courier New", monospace;
font-weight: 500;
font-size: 0.7rem;
color: var(--roxo-escuro);
background: var(--roxo-claro);
border-radius: 8px;
padding: 6px 8px;
word-break: break-word;
white-space: pre-wrap;
}
/* ---- 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; }
tr.dcr-linha-total td { font-weight: 700; }
.dcr-row-in { animation: dcrFadeIn 220ms ease; }
/* Destaque temporário nas linhas recém-reveladas ao expandir um grupo —
mesmo mecanismo de dc-conta-row--destaque em dashboard-contabil.js/css
do Portal (ver ali o porquê: dá pra acompanhar visualmente o que acabou
de aparecer ao expandir, e ao recolher de novo o destaque volta pra
leva anterior, ver pidDcrArvore() mais abaixo). Este documento não tem
alternância de tema (é sempre a marca clara do escritório), então só
existe a variante "clara": linhas fora do destaque ganham um leve
tingimento roxo (substitui o zebra por linha par de antes, que ficava
redundante); a leva recém-revelada fica em branco puro, lida como
"em foco" em vez de tingida — inclusive quando a linha revelada é um
grupo/total (`.dcr-linha-total`): no plano de contas real, a maioria
das linhas reveladas ao expandir um grupo também são grupos
(`tipo="S"`, com filhos mais fundo ainda) — excluir `.dcr-linha-total`
do destaque fazia o efeito nunca aparecer na prática (bug encontrado ao
testar: toda a leva revelada ao expandir "CAIXA E EQUIVALENTES DE
CAIXA" já é `.dcr-linha-total`).
Implementado via custom property por linha, não competindo por
especificidade CSS entre as 3 regras abaixo (tingimento/total/hover
têm a mesma especificidade — só o `td` de fato lê `--dcr-row-bg`, cujo
valor herda do `tr`; a regra de destaque vem por último de propósito,
pra vencer o empate por ordem de declaração quando uma linha é ao
mesmo tempo `.dcr-linha-total` e `.dcr-destaque`). Hover continua de
fora dos totais (nunca teve, decisão anterior à esta mudança) e vence
o destaque por ter uma regra mais específica (`:hover` conta como
classe a mais). */
.dcr-tabela tbody tr td {
background: var(--dcr-row-bg, transparent);
}
.dcr-tabela tbody tr:not(.dcr-linha-total) {
--dcr-row-bg: rgba(122, 91, 192, 0.05);
}
.dcr-tabela tbody tr.dcr-linha-total {
--dcr-row-bg: var(--dourado-claro);
}
.dcr-tabela tbody tr:not(.dcr-linha-total):hover {
--dcr-row-bg: #faf9ff;
}
.dcr-tabela tbody tr.dcr-destaque {
--dcr-row-bg: #fff;
}
.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; }
/* Ícone de observação na tabela do Balancete/DRE (coluna "Observação") —
só aparece na linha quando há mesmo uma observação visível ao cliente
(ver `oculta_no_relatorio` no template); clicar abre `.dcr-obs-inline-row`
logo abaixo, mesmo espírito do editor inline da tela de revisão do
Portal (`dc-conta-observacao-btn`/`dc-obs-edit-row`, dashboard-contabil.js). */
.dcr-obs-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
color: var(--roxo-medio);
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.dcr-obs-btn:hover { background: var(--roxo-claro); color: var(--roxo-escuro); }
.dcr-obs-btn.is-aberto { background: var(--roxo-claro); color: var(--roxo-escuro); }
.dcr-obs-inline-row td { background: #fff; padding: 12px 14px; }
.dcr-obs-inline {
border-left: 3px solid var(--roxo-medio);
padding: 8px 14px;
background: var(--roxo-claro);
border-radius: 0 8px 8px 0;
}
.dcr-obs-inline__rotulo {
display: block;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--roxo-escuro);
margin-bottom: 4px;
}
.dcr-obs-inline__texto { margin: 0; white-space: pre-wrap; color: var(--texto); }
/* ---- 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; }
/* Resumo do Fechamento — texto rico escrito pelo contador na aba
"Dashboard" da revisão (editor contenteditable, ver
dashboard-contabil.js), já sanitizado por nh3 antes de salvar
(ContabilResumoFechamentoSerializer) — só por isso é seguro usar
`|safe` aqui (primeiro `|safe` de template Django do projeto; até
agora todo texto rico só existia em tela SPA, injetado via
`innerHTML` no JS, nunca por um template do servidor). */
.dcr-resumo-fechamento {
background: #fff;
border: 1px solid var(--borda);
border-radius: 14px;
padding: 20px 22px;
box-shadow: var(--sombra);
margin-bottom: 22px;
animation: dcrFadeSlideUp 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.dcr-resumo-fechamento h2 { margin-bottom: 14px; }
.dcr-resumo-fechamento__texto { font-size: 0.88rem; line-height: 1.6; color: var(--texto); }
.dcr-resumo-fechamento__texto p { margin: 0 0 10px; }
.dcr-resumo-fechamento__texto p:last-child { margin-bottom: 0; }
.dcr-resumo-fechamento__texto ul, .dcr-resumo-fechamento__texto ol { margin: 0 0 10px; padding-left: 22px; }
.dcr-resumo-fechamento__texto img { max-width: 100%; border-radius: 8px; margin: 10px 0; }
.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-obs-btn { display: none; }
.dcr-card, .dcr-tabela-wrap, .dcr-obs-item, .dcr-resumo-fechamento { 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>Relatório 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">Resumo</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>
<th>Observação</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 %}" data-dcr-colapsado-padrao="{% if item.colapsado_padrao %}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{% if not item.colapsado_padrao %} is-expanded{% endif %}" 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>
<td>
{% if item.conta.observacao and not item.conta.oculta_no_relatorio %}
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="conta-{{ item.conta.id }}" aria-label="Ver observação">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
{% endif %}
</td>
</tr>
{% if item.conta.observacao and not item.conta.oculta_no_relatorio %}
<tr class="dcr-obs-inline-row" data-dcr-obs-row="conta-{{ item.conta.id }}" hidden>
<td colspan="7">
<div class="dcr-obs-inline">
<span class="dcr-obs-inline__rotulo">Observação do contador</span>
<p class="dcr-obs-inline__texto">{{ item.conta.observacao }}</p>
</div>
</td>
</tr>
{% endif %}
{% 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><th>Observação</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 %}" data-dcr-colapsado-padrao="{% if item.colapsado_padrao %}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{% if not item.colapsado_padrao %} is-expanded{% endif %}" 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>
<td>
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="linha-{{ item.linha.id }}" aria-label="Ver observação">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
{% endif %}
</td>
</tr>
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
<tr class="dcr-obs-inline-row" data-dcr-obs-row="linha-{{ item.linha.id }}" hidden>
<td colspan="3">
<div class="dcr-obs-inline">
<span class="dcr-obs-inline__rotulo">Observação do contador</span>
<p class="dcr-obs-inline__texto">{{ item.linha.observacao }}</p>
</div>
</td>
</tr>
{% endif %}
{% 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>
{% if apuracao.resumo_fechamento %}
<div class="dcr-secao dcr-resumo-fechamento">
<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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
Resumo do Fechamento
</h2>
<div class="dcr-resumo-fechamento__texto">{{ apuracao.resumo_fechamento|safe }}</div>
</div>
{% endif %}
<div class="dcr-secao">
{% for grupo in indicadores_grupos %}
<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 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg>
{{ grupo.nome }}
</h2>
<div class="dcr-cards">
{% for indicador in grupo.cards %}
<div class="dcr-card{% if indicador.dourado %} dcr-card--dourado{% endif %}" style="--i:{{ indicador.indice_animacao }}">
<div class="dcr-card-inner">
<div class="dcr-card-face dcr-card-face--front">
<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">{{ indicador.icone_svg }}</svg></div>
<div class="dcr-card__label">{{ indicador.nome }}</div>
<div
class="dcr-card__value"
data-final="{{ indicador.valor_formatado }}"
data-count="{{ indicador.valor_bruto }}"
data-format="{{ indicador.data_format }}"
{% if indicador.cor_regra %}data-color-rule="{{ indicador.cor_regra }}"{% endif %}
>{{ indicador.valor_formatado }}</div>
{% if indicador.nota %}<div class="dcr-card__nota">{{ indicador.nota }}</div>{% endif %}
</div>
<div class="dcr-card-face dcr-card-face--back">
<div class="dcr-card-face__titulo">{{ indicador.nome }}</div>
<p class="dcr-card-face__descricao">{{ indicador.descricao|default:"Sem descrição cadastrada." }}</p>
<div class="dcr-card-face__formula-label">Fórmula</div>
<code class="dcr-card-face__formula">{{ indicador.formula }}</code>
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</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;
// As linhas de observação inline (ver pidDcrObs abaixo) não entram
// nessa árvore — não têm nível/tem-filhos próprios, e deixá-las na
// lista quebraria a pilha de colapso (uma linha sem `data-dcr-nivel`
// vira NaN, que nunca compara >= nada). A visibilidade delas é
// sincronizada à parte, a partir do `hidden` da própria linha-pai.
var linhas = Array.prototype.slice.call(tbody.querySelectorAll("tr")).filter(function (tr) {
return !tr.hasAttribute("data-dcr-obs-row");
});
// Estado inicial: linhas marcadas pelo servidor como "colapsado por
// padrão" (a partir do nível equivalente ao grupo 4 do código de
// classificação, ver _CONTABIL_NIVEL_ABERTO_PADRAO em views.py)
// nascem recolhidas, sem precisar de nenhum clique.
var colapsadas = {};
linhas.forEach(function (tr) {
if (tr.getAttribute("data-dcr-colapsado-padrao") === "1") {
colapsadas[tr.getAttribute("data-dcr-id")] = true;
}
});
// Histórico do destaque — mesmo espírito de dcContasDestaqueHistorico
// em dashboard-contabil.js: cada expandir empilha o destaque atual e
// troca pros filhos diretos recém-revelados; cada recolher desempilha
// e restaura o destaque de antes, em vez de simplesmente apagar.
var historico = [];
function filhosDiretos(id) {
var idx = linhas.findIndex(function (tr) {
return tr.getAttribute("data-dcr-id") === id;
});
if (idx === -1) return {};
var nivelPai = Number(linhas[idx].getAttribute("data-dcr-nivel"));
var filhos = {};
for (var j = idx + 1; j < linhas.length; j++) {
var nivel = Number(linhas[j].getAttribute("data-dcr-nivel"));
if (nivel <= nivelPai) break;
if (nivel === nivelPai + 1) filhos[linhas[j].getAttribute("data-dcr-id")] = true;
}
return filhos;
}
// Destaque inicial (antes de qualquer clique) — pedido explícito do
// usuário depois de notar que folha genuína (linha sem filho
// nenhum, ex. "(-) SIMPLES NACIONAL") ficava de fora do destaque
// mesmo sendo visualmente tão "de baixo" quanto um grupo colapsado
// por padrão no mesmo nível. Usar só `colapsadas` (que só marca
// linha que TEM filho escondido) deixa de fora exatamente essas
// folhas, já que elas nunca entram nesse conjunto.
//
// Critério correto, que cobre os dois casos com a mesma regra:
// entre as linhas realmente visíveis com este `colapsadas`, uma
// linha entra no destaque se a próxima linha visível **não** for
// mais profunda que ela — nada foi revelado logo abaixo dela agora,
// seja porque está colapsada (filhos escondidos) ou porque é uma
// folha sem filho nenhum.
function calculaDestaqueInicial() {
var visiveis = [];
var pilha = [];
linhas.forEach(function (tr) {
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";
var temFilhoOculto = temFilhos && !!colapsadas[id];
if (temFilhoOculto) pilha.push(nivel);
if (!oculta) visiveis.push({ id: id, nivel: nivel, temFilhoOculto: temFilhoOculto });
});
var resultado = {};
visiveis.forEach(function (visivel, i) {
var proximo = visiveis[i + 1];
var eraFimDoRamo = !proximo || proximo.nivel <= visivel.nivel;
if (!eraFimDoRamo) return;
// Nível 0 só entra no destaque quando realmente esconde algo
// (grupo colapsado) — uma sequência de linhas de resultado
// irmãs no nível raiz da DRE não é o fim de um ramo aninhado,
// são linhas soltas lado a lado (mesmo ajuste de
// dcUltimaLevaVisivel() em dashboard-contabil.js).
if (visivel.nivel === 0 && !visivel.temFilhoOculto) return;
resultado[visivel.id] = true;
});
return resultado;
}
var destaque = calculaDestaqueInicial();
function aplicaDestaque() {
linhas.forEach(function (tr) {
tr.classList.toggle("dcr-destaque", !!destaque[tr.getAttribute("data-dcr-id")]);
});
}
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");
var estavaColapsada = !!colapsadas[id];
colapsadas[id] = !estavaColapsada;
btn.classList.toggle("is-expanded", !colapsadas[id]);
if (estavaColapsada) {
historico.push(destaque);
destaque = filhosDiretos(id);
} else {
destaque = historico.pop() || {};
}
aplicaDestaque();
atualiza();
});
aplicaDestaque();
atualiza();
}
// ---- Balancete/D.R.E.: painel de observação inline ----
// Ícone ao lado da descrição (só aparece quando a conta/linha tem uma
// observação que o contador marcou como visível ao cliente, ver
// `oculta_no_relatorio` — as ocultas nem chegam a este HTML) — clicar
// abre a linha logo abaixo com o texto, mesmo comportamento do editor
// inline da tela de revisão do Portal. Fica escondida por padrão
// (`hidden` no HTML) e some de novo se a linha-pai for escondida por
// um ancestral colapsado (`sincroniza()`, chamada depois de qualquer
// clique no corpo da tabela, inclusive os de expandir/recolher de
// `pidDcrArvore` — os dois listeners ficam no mesmo `tbody`).
function pidDcrObs(tbodyId) {
var tbody = document.getElementById(tbodyId);
if (!tbody) return;
var abertas = {};
function sincroniza() {
tbody.querySelectorAll("[data-dcr-obs-row]").forEach(function (obsRow) {
var id = obsRow.getAttribute("data-dcr-obs-row");
var linhaPai = obsRow.previousElementSibling;
var paiOculto = !linhaPai || linhaPai.hidden;
obsRow.hidden = paiOculto || !abertas[id];
});
}
tbody.addEventListener("click", function (event) {
var btn = event.target.closest("[data-dcr-obs-toggle]");
if (btn) {
var id = btn.getAttribute("data-dcr-obs-toggle");
abertas[id] = !abertas[id];
btn.classList.toggle("is-aberto", abertas[id]);
}
// Sincroniza sempre, não só quando o clique foi no próprio botão de
// observação — um clique em "recolher/expandir" (pidDcrArvore) pode
// ter acabado de esconder a linha-pai de um painel que estava aberto.
sincroniza();
});
sincroniza();
}
pidDcrArvore("dcr-balancete-body");
pidDcrObs("dcr-balancete-body");
pidDcrArvore("dcr-dre-body");
pidDcrObs("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>