portal_publico/templates/dashboard-contabil-relatorio.html

1489 lines
73 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">
<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; }
}
/* Clicar numa observação (seção acima da tabela) rola até a linha
referenciada e pisca esse destaque uma vez, pra deixar claro qual
linha é aquela — reinicia a cada clique via remove+reflow+add da
classe (ver pidDcrObsResumo() mais abaixo), não `transition`. */
@keyframes dcrRowFlash {
0%, 100% { background: var(--dcr-row-bg, transparent); }
30% { background: var(--dourado-claro); }
}
.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. */
/* "Exportar PDF" da aba Resumo (ver `resumo_pdf()` em views.py) fica
FORA de qualquer `<h2>` — a aba tem várias seções (Resumo do
Fechamento é condicional, indicadores é um loop de grupos), sem um
único cabeçalho pra pendurar o botão — por isso essa barra dedicada
no topo da aba, reaproveitando `.dcr-export-btn` (mesmo visual). */
.dcr-resumo-toolbar { display: flex; justify-content: flex-end; margin-bottom: 16px; }
.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); }
/* Botão "Limpar formatação" — mesmo estilo do de exportar (outline
discreto), mas em roxo pra não parecer uma segunda ação de exportação.
Fica ANTES do de exportar dentro do `<h2>` (quando os dois existem, só
Balancete/D.R.E.) e é quem carrega o `margin-left:auto` que empurra os
dois pro fim da linha — a Análise Vertical não tem exportar XLSX, então
esse botão sozinho já resolve sem precisar de wrapper. */
.dcr-reset-btn {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 999px;
border: 1.5px solid var(--roxo-medio);
color: var(--roxo-escuro);
background: #fff;
font-family: inherit;
font-size: 0.78rem;
font-weight: 700;
cursor: pointer;
transition: background 160ms ease, transform 160ms ease;
}
.dcr-reset-btn:hover { background: var(--roxo-claro); transform: translateY(-1px); }
.dcr-reset-btn:active { transform: translateY(0); }
/* Botão flutuante "Voltar ao topo" — canto inferior direito, sempre
presente no DOM (nunca via `hidden`, então `opacity`/`transform` com
`transition` normal já funciona, diferente do resto do documento que
usa `animation` pra reiniciar a partir de `display:none`/`hidden`).
Aparece só depois de rolar um pouco (ver `pidDcrScrollTop()`), pra não
poluir a tela logo no topo do relatório. */
.dcr-scroll-top-btn {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 20;
width: 44px;
height: 44px;
border-radius: 50%;
border: none;
background: linear-gradient(135deg, var(--roxo-medio), var(--roxo-escuro));
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 8px 20px -6px rgba(61, 33, 120, 0.55);
opacity: 0;
transform: translateY(8px);
pointer-events: none;
transition: opacity 200ms ease, transform 200ms ease, box-shadow 160ms ease;
}
.dcr-scroll-top-btn.is-visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
.dcr-scroll-top-btn:hover { box-shadow: 0 10px 24px -6px rgba(61, 33, 120, 0.7); }
/* ---- 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;
}
/* ---- 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-tabela tbody tr.dcr-row-flash td { animation: dcrRowFlash 1200ms 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; }
/* Ícone de observação na tabela do Balancete/DRE (coluna "Observação") —
só aparece na linha quando há mesmo uma observação vigente marcada como
visível ao cliente (`item.observacoes`, já filtrado na view a partir de
`ContabilObservacao.mostrar_ao_cliente`); 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); }
/* Só as observações com `ancora` (a conta/linha ainda existe nesta
apuração, ver `_com_ancora()` em views.py) ficam clicáveis — as sem
conta associada continuam com o mesmo hover sutil de sempre, sem
cursor de ponteiro nem contorno de foco, já que não têm pra onde ir. */
.dcr-obs-item--clicavel { cursor: pointer; }
.dcr-obs-item--clicavel:hover { border-color: var(--roxo-medio); }
.dcr-obs-item--clicavel:focus-visible { outline: 2px solid var(--roxo-medio); outline-offset: 2px; }
.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); }
/* Assinatura do histórico: quem escreveu, quando e (quando vem de um mês
anterior) em que competência foi registrada — ver `ContabilObservacao`. */
.dcr-obs-item__assinatura { font-size: 0.72rem; margin-top: 5px; color: var(--cinza-texto, #6b6b78); }
.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-resumo-toolbar { display: none; }
.dcr-reset-btn { display: none; }
.dcr-scroll-top-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>
{% if analise_vertical_meses %}
<button type="button" class="dcr-tab" data-dcr-tab="analise-vertical">Análise Vertical</button>
{% endif %}
<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"><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" id="dcr-obs-lista-balancete">
{% for obs in observacoes_contas %}
<li class="dcr-obs-item{% if obs.ancora %} dcr-obs-item--clicavel{% endif %}"{% if obs.ancora %} data-dcr-obs-ir="{{ obs.ancora }}" role="button" tabindex="0"{% endif %} 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">{{ obs.alvo_chave }} — {{ obs.alvo_rotulo }}</div>
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="dcr-vazio">Nenhuma observação registrada no Balancete.</p>
{% endif %}
</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"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
Balancete
<button type="button" class="dcr-reset-btn" data-dcr-reset="dcr-balancete-body">
<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="M3 12a9 9 0 1 0 2.64-6.36"/><path d="M3 3v6h6"/></svg>
Limpar formatação
</button>
<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.observacoes %}
<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.observacoes %}
<tr class="dcr-obs-inline-row" data-dcr-obs-row="conta-{{ item.conta.id }}" hidden>
<td colspan="7">
{% for obs in item.observacoes %}
<div class="dcr-obs-inline">
<span class="dcr-obs-inline__rotulo">Observação do contador · {{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · registrada na competência {{ obs.competencia_origem|competencia }}{% endif %}</span>
<p class="dcr-obs-inline__texto">{{ obs.texto }}</p>
</div>
{% endfor %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</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="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" id="dcr-obs-lista-dre">
{% for obs in observacoes_dre %}
<li class="dcr-obs-item{% if obs.ancora %} dcr-obs-item--clicavel{% endif %}"{% if obs.ancora %} data-dcr-obs-ir="{{ obs.ancora }}" role="button" tabindex="0"{% endif %} 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">{{ obs.alvo_rotulo }}</div>
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="dcr-vazio">Nenhuma observação registrada na D.R.E.</p>
{% endif %}
</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="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
<button type="button" class="dcr-reset-btn" data-dcr-reset="dcr-dre-body">
<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="M3 12a9 9 0 1 0 2.64-6.36"/><path d="M3 3v6h6"/></svg>
Limpar formatação
</button>
<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.observacoes %}
<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.observacoes %}
<tr class="dcr-obs-inline-row" data-dcr-obs-row="linha-{{ item.linha.id }}" hidden>
<td colspan="3">
{% for obs in item.observacoes %}
<div class="dcr-obs-inline">
<span class="dcr-obs-inline__rotulo">Observação do contador · {{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · registrada na competência {{ obs.competencia_origem|competencia }}{% endif %}</span>
<p class="dcr-obs-inline__texto">{{ obs.texto }}</p>
</div>
{% endfor %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% if analise_vertical_meses %}
<div class="dcr-tab-panel" data-dcr-panel="analise-vertical" 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="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 Análise Vertical
</h2>
{% if observacoes_analise_vertical %}
<ul class="dcr-obs-lista" id="dcr-obs-lista-av">
{% for obs in observacoes_analise_vertical %}
<li class="dcr-obs-item{% if obs.ancora %} dcr-obs-item--clicavel{% endif %}"{% if obs.ancora %} data-dcr-obs-ir="{{ obs.ancora }}" role="button" tabindex="0"{% endif %} 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="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg></div>
<div>
<div class="dcr-obs-item__titulo">{{ obs.alvo_rotulo }}</div>
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="dcr-vazio">Nenhuma observação registrada na Análise Vertical.</p>
{% endif %}
</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="M18 17V9M13 17V5M8 17v-3"/></svg>
Demonstração Mensal (Análise Vertical)
<button type="button" class="dcr-reset-btn" data-dcr-reset="dcr-av-body">
<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="M3 12a9 9 0 1 0 2.64-6.36"/><path d="M3 3v6h6"/></svg>
Limpar formatação
</button>
</h2>
<div class="dcr-tabela-wrap">
<table class="dcr-tabela">
<thead>
<tr>
<th>Descrição</th>
{% for mes in analise_vertical_meses %}
<th class="dcr-valor">{{ mes|capfirst }} — Valor</th>
<th class="dcr-valor">{{ mes|capfirst }} — Variação</th>
{% endfor %}
<th>Observação</th>
</tr>
</thead>
<tbody id="dcr-av-body">
{% for item in linhas_analise_vertical %}
<tr{% if item.linha.totalizador %} class="dcr-linha-total"{% endif %} data-dcr-nivel="{{ item.nivel }}" data-dcr-id="av-{{ 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>
{% for v in item.linha.valores %}
<td class="dcr-valor">{{ v.valor|moeda_av }}</td>
<td class="dcr-valor">{{ v.percentual|percentual_av }}</td>
{% endfor %}
<td>
{% if item.observacoes %}
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="av-{{ 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.observacoes %}
<tr class="dcr-obs-inline-row" data-dcr-obs-row="av-{{ item.linha.id }}" hidden>
<td colspan="{{ analise_vertical_meses|length|add:2 }}">
{% for obs in item.observacoes %}
<div class="dcr-obs-inline">
<span class="dcr-obs-inline__rotulo">Observação do contador · {{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · registrada na competência {{ obs.competencia_origem|competencia }}{% endif %}</span>
<p class="dcr-obs-inline__texto">{{ obs.texto }}</p>
</div>
{% endfor %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<div class="dcr-tab-panel" data-dcr-panel="indicadores" hidden>
<div class="dcr-resumo-toolbar">
<a class="dcr-export-btn" href="/api/contabil-apuracoes/{{ apuracao.id }}/resumo-pdf/">
<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="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"/><path d="M12 18v-6M9 15l3 3 3-3"/></svg>
Exportar PDF
</a>
</div>
{% 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="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 observacoes_analise_vertical or achados_com_observacao %}
<ul class="dcr-obs-lista">
{% for obs 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 — {{ obs.alvo_chave }} {{ obs.alvo_rotulo }}</div>
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
</div>
</li>
{% endfor %}
{% for obs 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. — {{ obs.alvo_rotulo }}</div>
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
</div>
</li>
{% endfor %}
{% for obs in observacoes_analise_vertical %}
<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="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg></div>
<div>
<div class="dcr-obs-item__titulo">Análise Vertical — {{ obs.alvo_rotulo }}</div>
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</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>
<button type="button" class="dcr-scroll-top-btn" id="dcr-scroll-top-btn" aria-label="Voltar ao topo" title="Voltar ao topo">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="M5 12l7-7 7 7"/></svg>
</button>
<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 cardsInicializados = 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);
// Os cards de indicador começam dentro de uma aba escondida
// (Balancete é a aba inicial) — a contagem animada não faz sentido
// rodar enquanto está invisível, então só dispara na primeira vez
// que a aba "Resumo" é aberta de fato.
if (alvo === "indicadores" && !cardsInicializados) {
cardsInicializados = true;
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();
// Cadeia de ancestrais de uma linha (do mais próximo ao mais distante)
// — usado por expandeAte() abaixo pra reabrir só o caminho necessário
// até uma conta/linha específica, sem mexer no resto da árvore.
function ancestrais(id) {
var idx = linhas.findIndex(function (tr) {
return tr.getAttribute("data-dcr-id") === id;
});
if (idx === -1) return [];
var nivelAtual = Number(linhas[idx].getAttribute("data-dcr-nivel"));
var resultado = [];
for (var j = idx - 1; j >= 0 && nivelAtual > 0; j--) {
var nivel = Number(linhas[j].getAttribute("data-dcr-nivel"));
if (nivel < nivelAtual) {
resultado.push(linhas[j].getAttribute("data-dcr-id"));
nivelAtual = nivel;
}
}
return resultado;
}
// Expande todo ancestral colapsado de `id` (sem mexer no destaque —
// não é um clique de "expandir grupo", é só tornar a linha visível
// pra poder rolar até ela) e devolve o `<tr>` correspondente, ou
// `undefined` se `id` não existir nesta árvore. Usada pelo clique
// numa observação da seção "Observações do Balancete/D.R.E./Análise
// Vertical" (ver pidDcrObsResumo() mais abaixo).
function expandeAte(id) {
var mudou = false;
ancestrais(id).forEach(function (ancestralId) {
if (colapsadas[ancestralId]) {
colapsadas[ancestralId] = false;
mudou = true;
var trAncestral = tbody.querySelector('[data-dcr-id="' + ancestralId + '"]');
var toggleBtn = trAncestral && trAncestral.querySelector("[data-dcr-toggle]");
if (toggleBtn) toggleBtn.classList.add("is-expanded");
}
});
if (mudou) atualiza();
return tbody.querySelector('[data-dcr-id="' + id + '"]');
}
// "Limpar formatação" (botão no cabeçalho da seção) — desfaz
// qualquer expandir/recolher/destaque que o usuário tenha feito
// (inclusive via clique numa observação) e volta pro estado inicial
// do servidor (`data-dcr-colapsado-padrao`), sem precisar recarregar
// a página. Não mexe nos painéis de observação inline abertos — isso
// é `pidDcrObs()`, chamado à parte (ver invocação de `pidDcrReset()`
// mais abaixo).
function resetar() {
linhas.forEach(function (tr) {
var id = tr.getAttribute("data-dcr-id");
var padrao = tr.getAttribute("data-dcr-colapsado-padrao") === "1";
colapsadas[id] = padrao;
var toggleBtn = tr.querySelector("[data-dcr-toggle]");
if (toggleBtn) toggleBtn.classList.toggle("is-expanded", !padrao);
tr.classList.remove("dcr-row-flash");
});
historico = [];
destaque = calculaDestaqueInicial();
aplicaDestaque();
atualiza();
}
return { expandeAte: expandeAte, resetar: resetar };
}
// ---- Balancete/D.R.E.: painel de observação inline ----
// Ícone ao lado da descrição (só aparece quando a conta/linha tem
// observação vigente marcada como visível ao cliente, ver
// `ContabilObservacao.mostrar_ao_cliente` — as internas nem chegam a
// este HTML) — clicar abre a linha logo abaixo com o texto (pode ter
// mais de uma, cada uma assinada), mesmo comportamento do painel
// 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();
// Fecha todo painel de observação inline aberto — parte do "Limpar
// formatação" (ver pidDcrReset() mais abaixo), que também chama
// arvore.resetar() da mesma tabela.
function fecharTudo() {
abertas = {};
tbody.querySelectorAll("[data-dcr-obs-toggle]").forEach(function (btn) {
btn.classList.remove("is-aberto");
});
sincroniza();
}
return { fecharTudo: fecharTudo };
}
// ---- Observações (seção acima da tabela): clique leva até a conta/
// linha referenciada ----
// `container` é a `<ul class="dcr-obs-lista">` da seção; só os `<li>`
// com `data-dcr-obs-ir` são clicáveis (a view só preenche `obs.ancora`
// quando a conta/linha ainda existe nesta apuração — "se houver" a
// conta, ver `_com_ancora()` em views.py). `arvore` é o retorno de
// `pidDcrArvore()` da mesma tabela (Balancete/D.R.E./Análise Vertical
// sempre ficam na mesma aba que sua própria seção de observações,
// nunca precisa trocar de aba pra chegar na linha).
function pidDcrObsResumo(container, arvore) {
if (!container || !arvore) return;
function irPara(id) {
var tr = arvore.expandeAte(id);
if (!tr) return;
tr.scrollIntoView({ behavior: "smooth", block: "center" });
tr.classList.remove("dcr-row-flash");
void tr.offsetWidth; // força reflow pra reiniciar a animação
tr.classList.add("dcr-row-flash");
}
container.addEventListener("click", function (event) {
var item = event.target.closest("[data-dcr-obs-ir]");
if (item) irPara(item.getAttribute("data-dcr-obs-ir"));
});
// Teclado (role="button"/tabindex já no HTML) — Enter/Espaço ativam
// igual a um clique, mesmo padrão de acessibilidade de um botão real.
container.addEventListener("keydown", function (event) {
if (event.key !== "Enter" && event.key !== " ") return;
var item = event.target.closest("[data-dcr-obs-ir]");
if (!item) return;
event.preventDefault();
irPara(item.getAttribute("data-dcr-obs-ir"));
});
}
var dcrArvoreBalancete = pidDcrArvore("dcr-balancete-body");
var dcrObsBalancete = pidDcrObs("dcr-balancete-body");
var dcrArvoreDre = pidDcrArvore("dcr-dre-body");
var dcrObsDre = pidDcrObs("dcr-dre-body");
var dcrArvoreAv = pidDcrArvore("dcr-av-body");
var dcrObsAv = pidDcrObs("dcr-av-body");
pidDcrObsResumo(document.getElementById("dcr-obs-lista-balancete"), dcrArvoreBalancete);
pidDcrObsResumo(document.getElementById("dcr-obs-lista-dre"), dcrArvoreDre);
pidDcrObsResumo(document.getElementById("dcr-obs-lista-av"), dcrArvoreAv);
// ---- Botão "Limpar formatação" (um por tabela) ----
// Volta a árvore (expandido/recolhido/destaque) e os painéis de
// observação inline daquela tabela pro estado com que a página
// carregou, sem precisar recarregar nada. `data-dcr-reset` no botão
// carrega o mesmo id do `tbody` — é a chave usada nos dois mapas logo
// abaixo pra achar a árvore/painel de observação certos.
var dcrArvores = { "dcr-balancete-body": dcrArvoreBalancete, "dcr-dre-body": dcrArvoreDre, "dcr-av-body": dcrArvoreAv };
var dcrObsControles = { "dcr-balancete-body": dcrObsBalancete, "dcr-dre-body": dcrObsDre, "dcr-av-body": dcrObsAv };
document.querySelectorAll("[data-dcr-reset]").forEach(function (btn) {
btn.addEventListener("click", function () {
var tbodyId = btn.getAttribute("data-dcr-reset");
var arvore = dcrArvores[tbodyId];
var obsControle = dcrObsControles[tbodyId];
if (arvore) arvore.resetar();
if (obsControle) obsControle.fecharTudo();
});
});
// ---- Botão "Voltar ao topo" ----
// Aparece só depois de rolar (`PID_DCR_SCROLL_TOP_LIMIAR`), pra não
// ficar poluindo a tela logo no início do relatório.
(function () {
var PID_DCR_SCROLL_TOP_LIMIAR = 320;
var btn = document.getElementById("dcr-scroll-top-btn");
if (!btn) return;
window.addEventListener("scroll", function () {
btn.classList.toggle("is-visible", window.scrollY > PID_DCR_SCROLL_TOP_LIMIAR);
});
btn.addEventListener("click", function () {
window.scrollTo({ top: 0, behavior: "smooth" });
});
})();
// ---- 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);
});
}
// Impressão mostra as 4 abas seguidas (ver @media print). Dois casos:
// (1) "Resumo" nunca foi clicada na tela — cards ainda não existem,
// então monta agora, 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 (!cardsInicializados) {
cardsInicializados = true;
pidDcrAnimaContadores(true);
} else {
finalizadores.forEach(function (fn) { fn(); });
}
});
})();
</script>
</body>
</html>