860 lines
38 KiB
HTML
860 lines
38 KiB
HTML
{% load static %}
|
|
{% load contabil_extras %}
|
|
<!doctype html>
|
|
<html lang="pt-br">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Dashboard Contábil — {{ apuracao.nome_empresa }} — {{ apuracao.competencia|competencia }}</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800&display=swap" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
|
<style>
|
|
:root {
|
|
--roxo-escuro: #3d2178;
|
|
--roxo-mais-escuro: #281552;
|
|
--roxo-claro: #efe8ff;
|
|
--roxo-medio: #7a5bc0;
|
|
--dourado: #b4872a;
|
|
--dourado-escuro: #8f6a1f;
|
|
--dourado-claro: #f6ecd4;
|
|
--dourado-vivo: #d9ac48;
|
|
--texto: #211d2b;
|
|
--texto-muted: #6b6478;
|
|
--borda: #e6e1f0;
|
|
--bg-canvas: #f5f3fa;
|
|
--sucesso-bg: #dcf5e3;
|
|
--sucesso-texto: #1a7a3d;
|
|
--falha-bg: #fbe2e2;
|
|
--falha-texto: #b3261e;
|
|
--aviso-bg: #fdf0d8;
|
|
--aviso-texto: #9a6a0a;
|
|
--sombra: 0 1px 2px rgba(40, 21, 82, 0.04), 0 8px 24px -12px rgba(40, 21, 82, 0.18);
|
|
--sombra-hover: 0 4px 10px rgba(40, 21, 82, 0.06), 0 16px 36px -14px rgba(40, 21, 82, 0.26);
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html { scroll-behavior: smooth; }
|
|
body {
|
|
margin: 0;
|
|
background:
|
|
radial-gradient(circle at 15% -10%, rgba(122, 91, 192, 0.14), transparent 42%),
|
|
radial-gradient(circle at 100% 0%, rgba(180, 135, 42, 0.10), transparent 38%),
|
|
var(--bg-canvas);
|
|
color: var(--texto);
|
|
font-family: "Inter", "Segoe UI", Arial, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
h1, h2, .dcr-card__value, .dcr-tab { font-family: "Manrope", "Inter", sans-serif; }
|
|
|
|
/* ---- Animações de entrada — nunca fixar opacity:0 fora de @keyframes,
|
|
só via `animation ... both`, pra que @media print (que zera toda
|
|
`animation`) volte sozinho ao estado normal, sem precisar de reset
|
|
extra. Mesmo espírito do "animation, não transition, pra reiniciar a
|
|
partir de display:none" já documentado no Portal (CLAUDE.md,
|
|
"Animações"), embora este documento seja autocontido. */
|
|
@keyframes dcrFadeSlideUp {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
@keyframes dcrFadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
@keyframes dcrScaleIn {
|
|
from { opacity: 0; transform: scale(0.96); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
@keyframes dcrGlow {
|
|
0%, 100% { opacity: 0.55; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
.dcr-page { max-width: 1140px; margin: 0 auto; padding: 0 24px 56px; }
|
|
|
|
/* ---- Cabeçalho ---- */
|
|
.dcr-header {
|
|
position: relative;
|
|
overflow: hidden;
|
|
background: linear-gradient(135deg, var(--roxo-escuro) 0%, var(--roxo-mais-escuro) 100%);
|
|
color: #fff;
|
|
padding: 30px 28px;
|
|
margin: 0 -24px 28px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 22px;
|
|
flex-wrap: wrap;
|
|
border-bottom: 3px solid var(--dourado);
|
|
animation: dcrFadeIn 500ms ease both;
|
|
}
|
|
.dcr-header::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
background: radial-gradient(circle at 85% -20%, rgba(217, 172, 72, 0.35), transparent 55%);
|
|
pointer-events: none;
|
|
animation: dcrGlow 6s ease-in-out infinite;
|
|
}
|
|
.dcr-header__brand { display: flex; align-items: center; gap: 18px; position: relative; z-index: 1; }
|
|
.dcr-header img { height: 52px; width: auto; filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.25)); }
|
|
.dcr-header h1 { margin: 0; font-size: 1.4rem; font-weight: 800; letter-spacing: -0.01em; }
|
|
.dcr-header p { margin: 4px 0 0; color: var(--dourado-claro); font-size: 0.86rem; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
.dcr-status-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 2px 10px;
|
|
border-radius: 999px;
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
background: rgba(255, 255, 255, 0.14);
|
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
}
|
|
.dcr-status-pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
.dcr-status-pill--concluida { color: #7CE7A0; }
|
|
.dcr-status-pill--revisao { color: #F5D583; }
|
|
.dcr-print-btn {
|
|
margin-left: auto;
|
|
position: relative;
|
|
z-index: 1;
|
|
background: linear-gradient(135deg, var(--dourado-vivo), var(--dourado));
|
|
color: var(--roxo-mais-escuro);
|
|
border: none;
|
|
border-radius: 8px;
|
|
padding: 11px 20px;
|
|
font-family: inherit;
|
|
font-size: 0.86rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
box-shadow: 0 6px 18px -6px rgba(217, 172, 72, 0.7);
|
|
transition: transform 160ms ease, box-shadow 160ms ease;
|
|
}
|
|
.dcr-print-btn:hover { transform: translateY(-2px); box-shadow: 0 10px 24px -6px rgba(217, 172, 72, 0.85); }
|
|
.dcr-print-btn:active { transform: translateY(0); }
|
|
|
|
/* Botão de exportar XLSX — dentro do próprio <h2> da seção (Balancete/DRE),
|
|
que já é flex, então margin-left:auto sozinho empurra pro fim da linha
|
|
(mesmo truque do .dcr-print-btn dentro do header). Mais discreto que o
|
|
botão de imprimir (outline, não preenchido) por ser uma ação
|
|
secundária por seção, não a ação principal do documento. */
|
|
.dcr-export-btn {
|
|
margin-left: auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 14px;
|
|
border-radius: 999px;
|
|
border: 1.5px solid var(--dourado);
|
|
color: var(--roxo-escuro);
|
|
background: #fff;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
text-decoration: none;
|
|
transition: background 160ms ease, transform 160ms ease;
|
|
}
|
|
.dcr-export-btn:hover { background: var(--dourado-claro); transform: translateY(-1px); }
|
|
.dcr-export-btn:active { transform: translateY(0); }
|
|
|
|
/* ---- Abas ---- */
|
|
.dcr-tabs {
|
|
position: relative;
|
|
display: flex;
|
|
gap: 4px;
|
|
border-bottom: 2px solid var(--borda);
|
|
margin-bottom: 26px;
|
|
animation: dcrFadeSlideUp 460ms ease 80ms both;
|
|
}
|
|
.dcr-tab {
|
|
position: relative;
|
|
z-index: 1;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 12px 20px;
|
|
font-size: 0.92rem;
|
|
font-weight: 700;
|
|
color: var(--texto-muted);
|
|
transition: color 160ms ease;
|
|
}
|
|
.dcr-tab:hover { color: var(--roxo-escuro); }
|
|
.dcr-tab.is-active { color: var(--roxo-escuro); }
|
|
.dcr-tabs__indicator {
|
|
position: absolute;
|
|
bottom: -2px;
|
|
left: 0;
|
|
height: 3px;
|
|
border-radius: 3px 3px 0 0;
|
|
background: linear-gradient(90deg, var(--dourado), var(--dourado-vivo));
|
|
transition: transform 280ms cubic-bezier(0.65, 0, 0.35, 1), width 280ms cubic-bezier(0.65, 0, 0.35, 1);
|
|
}
|
|
|
|
.dcr-tab-panel { animation: dcrFadeSlideUp 420ms ease 140ms both; }
|
|
|
|
.dcr-secao { margin-bottom: 34px; }
|
|
.dcr-secao h2 {
|
|
font-size: 1.02rem;
|
|
font-weight: 700;
|
|
color: var(--roxo-escuro);
|
|
border-bottom: 2px solid var(--borda);
|
|
padding-bottom: 9px;
|
|
margin: 0 0 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.dcr-secao h2 svg { color: var(--dourado); flex-shrink: 0; }
|
|
|
|
/* ---- Cards de indicador ---- */
|
|
.dcr-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 22px;
|
|
}
|
|
.dcr-card {
|
|
position: relative;
|
|
background: #fff;
|
|
border: 1px solid var(--borda);
|
|
border-radius: 14px;
|
|
padding: 16px 18px;
|
|
box-shadow: var(--sombra);
|
|
transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
|
|
animation: dcrFadeSlideUp 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
animation-delay: calc(var(--i, 0) * 55ms + 60ms);
|
|
}
|
|
.dcr-card:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: var(--sombra-hover);
|
|
border-color: rgba(122, 91, 192, 0.35);
|
|
}
|
|
.dcr-card__icon {
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 12px;
|
|
background: var(--roxo-claro);
|
|
color: var(--roxo-escuro);
|
|
}
|
|
.dcr-card--dourado .dcr-card__icon { background: var(--dourado-claro); color: var(--dourado-escuro); }
|
|
.dcr-card__label { font-size: 0.74rem; color: var(--texto-muted); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; }
|
|
.dcr-card__value {
|
|
font-size: 1.55rem;
|
|
font-weight: 800;
|
|
color: var(--roxo-escuro);
|
|
margin-top: 4px;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
.dcr-card__value.dcr-valor-positivo { color: var(--sucesso-texto); }
|
|
.dcr-card__value.dcr-valor-negativo { color: var(--falha-texto); }
|
|
.dcr-card__nota { font-size: 0.7rem; color: var(--texto-muted); margin-top: 6px; line-height: 1.35; }
|
|
|
|
/* ---- Gráfico ---- */
|
|
.dcr-chart-wrap {
|
|
background: #fff;
|
|
border: 1px solid var(--borda);
|
|
border-radius: 14px;
|
|
padding: 20px;
|
|
box-shadow: var(--sombra);
|
|
animation: dcrFadeSlideUp 460ms ease 120ms both;
|
|
}
|
|
|
|
/* ---- Tabelas ---- */
|
|
.dcr-tabela-wrap {
|
|
background: #fff;
|
|
border: 1px solid var(--borda);
|
|
border-radius: 14px;
|
|
overflow: hidden;
|
|
box-shadow: var(--sombra);
|
|
animation: dcrScaleIn 380ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
}
|
|
table.dcr-tabela { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
table.dcr-tabela th, table.dcr-tabela td { padding: 9px 14px; border-bottom: 1px solid var(--borda); text-align: left; }
|
|
table.dcr-tabela th {
|
|
background: linear-gradient(180deg, var(--roxo-claro), #f4f0ff);
|
|
color: var(--roxo-escuro);
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
position: sticky;
|
|
top: 0;
|
|
}
|
|
table.dcr-tabela td.dcr-valor { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
table.dcr-tabela tbody tr { transition: background 120ms ease; }
|
|
table.dcr-tabela tbody tr:not(.dcr-linha-total):hover { background: #faf9ff; }
|
|
table.dcr-tabela tbody tr:nth-child(even):not(.dcr-linha-total) { background: rgba(122, 91, 192, 0.025); }
|
|
tr.dcr-linha-total td { font-weight: 700; background: var(--dourado-claro); }
|
|
.dcr-row-in { animation: dcrFadeIn 220ms ease; }
|
|
|
|
.dcr-desc-cell { display: flex; align-items: center; gap: 7px; }
|
|
.dcr-toggle {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 18px;
|
|
height: 18px;
|
|
flex-shrink: 0;
|
|
border-radius: 5px;
|
|
color: var(--texto-muted);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
}
|
|
.dcr-toggle:hover { color: var(--roxo-escuro); background: var(--roxo-claro); }
|
|
.dcr-toggle svg { transition: transform 160ms cubic-bezier(0.65, 0, 0.35, 1); }
|
|
.dcr-toggle.is-expanded svg { transform: rotate(90deg); }
|
|
.dcr-toggle-spacer { display: inline-block; width: 18px; height: 18px; flex-shrink: 0; }
|
|
|
|
/* ---- Observações ---- */
|
|
.dcr-obs-lista { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
|
|
.dcr-obs-item {
|
|
display: flex;
|
|
gap: 12px;
|
|
background: #fff;
|
|
border: 1px solid var(--borda);
|
|
border-radius: 12px;
|
|
padding: 13px 16px;
|
|
box-shadow: var(--sombra);
|
|
animation: dcrFadeSlideUp 380ms ease both;
|
|
animation-delay: calc(var(--i, 0) * 45ms);
|
|
transition: box-shadow 160ms ease, transform 160ms ease;
|
|
}
|
|
.dcr-obs-item:hover { transform: translateX(2px); box-shadow: var(--sombra-hover); }
|
|
.dcr-obs-item__icone {
|
|
flex-shrink: 0;
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--dourado-claro);
|
|
color: var(--dourado-escuro);
|
|
}
|
|
.dcr-obs-item__titulo { font-weight: 700; font-size: 0.85rem; color: var(--roxo-escuro); }
|
|
.dcr-obs-item__texto { font-size: 0.85rem; margin-top: 3px; white-space: pre-wrap; color: var(--texto); }
|
|
.dcr-vazio { color: var(--texto-muted); font-size: 0.85rem; font-style: italic; }
|
|
.dcr-footer { color: var(--texto-muted); font-size: 0.76rem; text-align: center; margin-top: 28px; }
|
|
|
|
@media (max-width: 640px) {
|
|
.dcr-header { padding: 22px 20px; }
|
|
.dcr-tabs { overflow-x: auto; }
|
|
}
|
|
|
|
@media print {
|
|
* { animation: none !important; transition: none !important; }
|
|
body { background: #fff; }
|
|
.dcr-print-btn { display: none; }
|
|
.dcr-export-btn { display: none; }
|
|
.dcr-header { margin: 0 0 24px; }
|
|
.dcr-tabs { display: none; }
|
|
.dcr-tab-panel { display: block !important; }
|
|
.dcr-tab-panel:not(:last-child) { page-break-after: always; }
|
|
.dcr-tabela tbody tr[hidden] { display: table-row !important; }
|
|
.dcr-toggle { display: none; }
|
|
.dcr-card, .dcr-tabela-wrap, .dcr-obs-item { box-shadow: none !important; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="dcr-page">
|
|
<header class="dcr-header">
|
|
<div class="dcr-header__brand">
|
|
<img src="{% static 'img/logo-branco.png' %}" alt="De Paula Contadores">
|
|
<div>
|
|
<h1>Dashboard Contábil — {{ apuracao.nome_empresa }}</h1>
|
|
<p>
|
|
CNPJ {{ apuracao.cnpj|default:"não informado" }} · Competência {{ apuracao.competencia|competencia }}
|
|
<span class="dcr-status-pill dcr-status-pill--{{ apuracao.status }}">{{ apuracao.get_status_display }}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="dcr-print-btn" onclick="window.print()">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
|
Imprimir
|
|
</button>
|
|
</header>
|
|
|
|
<nav class="dcr-tabs">
|
|
<span class="dcr-tabs__indicator" id="dcr-tabs-indicator"></span>
|
|
<button type="button" class="dcr-tab is-active" data-dcr-tab="balancete">Balancete</button>
|
|
<button type="button" class="dcr-tab" data-dcr-tab="dre">D.R.E.</button>
|
|
<button type="button" class="dcr-tab" data-dcr-tab="indicadores">Indicadores</button>
|
|
</nav>
|
|
|
|
<div class="dcr-tab-panel" data-dcr-panel="balancete">
|
|
<div class="dcr-secao">
|
|
<h2>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
|
Balancete
|
|
<a class="dcr-export-btn" href="/api/contabil-apuracoes/{{ apuracao.id }}/exportar-xlsx/?parte=balancete">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
|
|
Exportar XLSX
|
|
</a>
|
|
</h2>
|
|
<div class="dcr-tabela-wrap">
|
|
<table class="dcr-tabela">
|
|
<thead>
|
|
<tr>
|
|
<th>Conta</th>
|
|
<th>Descrição</th>
|
|
<th class="dcr-valor">Saldo Anterior</th>
|
|
<th class="dcr-valor">Débito</th>
|
|
<th class="dcr-valor">Crédito</th>
|
|
<th class="dcr-valor">Saldo Atual</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="dcr-balancete-body">
|
|
{% for item in contas %}
|
|
<tr{% if item.conta.tipo == "S" %} class="dcr-linha-total"{% endif %} data-dcr-nivel="{{ item.nivel }}" data-dcr-id="conta-{{ item.conta.id }}" data-dcr-tem-filhos="{% if item.tem_filhos %}1{% else %}0{% endif %}" 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>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dcr-secao">
|
|
<h2>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/></svg>
|
|
Observações do Balancete
|
|
</h2>
|
|
{% if observacoes_contas %}
|
|
<ul class="dcr-obs-lista">
|
|
{% for conta in observacoes_contas %}
|
|
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
|
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg></div>
|
|
<div>
|
|
<div class="dcr-obs-item__titulo">{{ conta.codigo }} — {{ conta.descricao }}</div>
|
|
<div class="dcr-obs-item__texto">{{ conta.observacao }}</div>
|
|
</div>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="dcr-vazio">Nenhuma observação registrada no Balancete.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dcr-tab-panel" data-dcr-panel="dre" hidden>
|
|
<div class="dcr-secao">
|
|
<h2>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5a2 2 0 0 1 2-2h8l6 6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/><path d="M14 3v6h6M9 13h6M9 17h6"/></svg>
|
|
D.R.E. — Demonstração do Resultado do Exercício
|
|
<a class="dcr-export-btn" href="/api/contabil-apuracoes/{{ apuracao.id }}/exportar-xlsx/?parte=dre">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
|
|
Exportar XLSX
|
|
</a>
|
|
</h2>
|
|
<div class="dcr-tabela-wrap">
|
|
<table class="dcr-tabela">
|
|
<thead><tr><th>Descrição</th><th class="dcr-valor">Valor</th></tr></thead>
|
|
<tbody id="dcr-dre-body">
|
|
{% for item in linhas_dre %}
|
|
<tr{% if item.linha.totalizador %} class="dcr-linha-total"{% endif %} data-dcr-nivel="{{ item.nivel }}" data-dcr-id="linha-{{ item.linha.id }}" data-dcr-tem-filhos="{% if item.tem_filhos %}1{% else %}0{% endif %}" 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>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dcr-secao">
|
|
<h2>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/></svg>
|
|
Observações da D.R.E.
|
|
</h2>
|
|
{% if observacoes_dre %}
|
|
<ul class="dcr-obs-lista">
|
|
{% for linha in observacoes_dre %}
|
|
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
|
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5a2 2 0 0 1 2-2h8l6 6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/><path d="M14 3v6h6"/></svg></div>
|
|
<div>
|
|
<div class="dcr-obs-item__titulo">{{ linha.descricao }}</div>
|
|
<div class="dcr-obs-item__texto">{{ linha.observacao }}</div>
|
|
</div>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% else %}
|
|
<p class="dcr-vazio">Nenhuma observação registrada na D.R.E.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dcr-tab-panel" data-dcr-panel="indicadores" hidden>
|
|
<div class="dcr-secao">
|
|
{% 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__icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></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>
|
|
{% 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;
|
|
var linhas = Array.prototype.slice.call(tbody.querySelectorAll("tr"));
|
|
// 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;
|
|
}
|
|
});
|
|
|
|
function atualiza() {
|
|
var pilha = [];
|
|
linhas.forEach(function (tr) {
|
|
var eraOculta = tr.hidden;
|
|
var nivel = Number(tr.getAttribute("data-dcr-nivel"));
|
|
while (pilha.length && pilha[pilha.length - 1] >= nivel) pilha.pop();
|
|
var oculta = pilha.length > 0;
|
|
var id = tr.getAttribute("data-dcr-id");
|
|
var temFilhos = tr.getAttribute("data-dcr-tem-filhos") === "1";
|
|
if (temFilhos && colapsadas[id]) pilha.push(nivel);
|
|
tr.hidden = oculta;
|
|
if (eraOculta && !oculta) {
|
|
tr.classList.remove("dcr-row-in");
|
|
void tr.offsetWidth; // força reflow pra reiniciar a animação
|
|
tr.classList.add("dcr-row-in");
|
|
}
|
|
});
|
|
}
|
|
|
|
tbody.addEventListener("click", function (event) {
|
|
var btn = event.target.closest("[data-dcr-toggle]");
|
|
if (!btn) return;
|
|
var tr = btn.closest("tr");
|
|
var id = tr.getAttribute("data-dcr-id");
|
|
colapsadas[id] = !colapsadas[id];
|
|
btn.classList.toggle("is-expanded", !colapsadas[id]);
|
|
atualiza();
|
|
});
|
|
|
|
atualiza();
|
|
}
|
|
|
|
pidDcrArvore("dcr-balancete-body");
|
|
pidDcrArvore("dcr-dre-body");
|
|
|
|
// ---- Cards: contagem animada + cor por sinal/liquidez ----
|
|
// O texto final exibido é sempre `data-final` (a string já formatada
|
|
// pelos filtros `moeda`/`percentual`/`indice` no servidor) — a
|
|
// animação só interpola os quadros intermediários com Intl nativo do
|
|
// navegador; nunca recalcula o valor de verdade, só decora a
|
|
// transição até ele. `finalizadores` guarda uma função por card pra
|
|
// pular direto pro valor final — usado no `beforeprint`, pra nunca
|
|
// imprimir um card a meio de uma animação.
|
|
var finalizadores = [];
|
|
|
|
function formataIntermediario(valor, formato) {
|
|
if (formato === "pct") {
|
|
return (valor * 100).toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + "%";
|
|
}
|
|
if (formato === "moeda") {
|
|
return valor.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
|
|
}
|
|
return valor.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
}
|
|
|
|
function aplicaCor(el, valor) {
|
|
var regra = el.getAttribute("data-color-rule");
|
|
if (!regra || isNaN(valor)) return;
|
|
el.classList.remove("dcr-valor-positivo", "dcr-valor-negativo");
|
|
if (regra === "sign") {
|
|
if (valor > 0) el.classList.add("dcr-valor-positivo");
|
|
else if (valor < 0) el.classList.add("dcr-valor-negativo");
|
|
} else if (regra === "liquidez") {
|
|
el.classList.add(valor >= 1 ? "dcr-valor-positivo" : "dcr-valor-negativo");
|
|
}
|
|
}
|
|
|
|
// `instantaneo` pula a animação e escreve o valor final direto — usado
|
|
// no fallback de impressão (ver `beforeprint` mais abaixo), quando a
|
|
// aba "Indicadores" nunca foi aberta na tela: sem isso, a impressão
|
|
// poderia capturar os cards no meio da contagem (RAF não termina a
|
|
// tempo do `window.print()` seguir em frente).
|
|
function pidDcrAnimaContadores(instantaneo) {
|
|
document.querySelectorAll(".dcr-card__value[data-count]").forEach(function (el) {
|
|
var bruto = el.getAttribute("data-count");
|
|
var final = el.getAttribute("data-final");
|
|
var formato = el.getAttribute("data-format");
|
|
if (bruto === "" || bruto === null) return; // indisponível ("—") — nada a animar
|
|
var alvo = parseFloat(bruto);
|
|
aplicaCor(el, alvo);
|
|
if (instantaneo) {
|
|
el.textContent = final;
|
|
return;
|
|
}
|
|
var inicio = null;
|
|
var duracao = 800;
|
|
function passo(agora) {
|
|
if (inicio === null) inicio = agora;
|
|
var progresso = Math.min(1, (agora - inicio) / duracao);
|
|
var suavizado = 1 - Math.pow(1 - progresso, 3); // easeOutCubic
|
|
el.textContent = formataIntermediario(alvo * suavizado, formato);
|
|
if (progresso < 1) {
|
|
requestAnimationFrame(passo);
|
|
} else {
|
|
el.textContent = final;
|
|
}
|
|
}
|
|
finalizadores.push(function () { el.textContent = final; });
|
|
requestAnimationFrame(passo);
|
|
});
|
|
}
|
|
|
|
// ---- Gráfico de evolução do Resultado Líquido ----
|
|
function inicializaGrafico() {
|
|
var dados = JSON.parse(document.getElementById("dcr-evolucao-data").textContent);
|
|
var canvas = document.getElementById("dcr-grafico-evolucao");
|
|
if (!canvas || dados.length < 2 || typeof Chart === "undefined") return;
|
|
var ctx = canvas.getContext("2d");
|
|
var gradiente = ctx.createLinearGradient(0, 0, 0, canvas.height || 260);
|
|
gradiente.addColorStop(0, "rgba(61, 33, 120, 0.28)");
|
|
gradiente.addColorStop(1, "rgba(61, 33, 120, 0.02)");
|
|
Chart.defaults.font.family = "Inter, sans-serif";
|
|
new Chart(ctx, {
|
|
type: "line",
|
|
data: {
|
|
labels: dados.map(function (p) { return p.competencia; }),
|
|
datasets: [{
|
|
label: "Resultado Líquido (R$)",
|
|
data: dados.map(function (p) { return parseFloat(p.resultado_liquido); }),
|
|
borderColor: "#3d2178",
|
|
backgroundColor: gradiente,
|
|
pointBackgroundColor: "#b4872a",
|
|
pointBorderColor: "#fff",
|
|
pointBorderWidth: 2,
|
|
pointRadius: 4,
|
|
pointHoverRadius: 6,
|
|
borderWidth: 2.5,
|
|
tension: 0.35,
|
|
fill: true,
|
|
}],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
animation: { duration: 900, easing: "easeOutCubic" },
|
|
interaction: { intersect: false, mode: "index" },
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
backgroundColor: "#3d2178",
|
|
titleFont: { weight: "700" },
|
|
padding: 10,
|
|
cornerRadius: 8,
|
|
callbacks: {
|
|
label: function (item) { return "R$ " + item.parsed.y.toLocaleString("pt-BR", { minimumFractionDigits: 2 }); },
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: { grid: { display: false } },
|
|
y: {
|
|
grid: { color: "rgba(228, 224, 236, 0.7)" },
|
|
ticks: { callback: function (v) { return "R$ " + v.toLocaleString("pt-BR"); } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
// Impressão mostra as 3 abas seguidas (ver @media print). Dois casos:
|
|
// (1) "Indicadores" nunca foi clicada na tela — gráfico e cards ainda
|
|
// não existem, então monta os dois agora, com os cards já no valor
|
|
// final (sem animação, ver `pidDcrAnimaContadores(true)` acima); (2)
|
|
// já foi clicada e a contagem pode estar em andamento — só finaliza
|
|
// os valores já em animação. Um único listener, pra nunca competir
|
|
// com o outro caso.
|
|
window.addEventListener("beforeprint", function () {
|
|
if (!graficoInicializado) {
|
|
graficoInicializado = true;
|
|
inicializaGrafico();
|
|
pidDcrAnimaContadores(true);
|
|
} else {
|
|
finalizadores.forEach(function (fn) { fn(); });
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|