pra parecer texto clicável — abre "Ver fórmula" (ver
+ data-dc-hub-ver em dashboard-contabil.js). */
+.dc-ind-hub-item__nome {
+ flex: 1;
+ min-width: 0;
+ text-align: left;
+ background: none;
+ border: none;
+ padding: 0;
+ font-size: 0.85rem;
+ color: var(--text-primary);
+ cursor: pointer;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.dc-ind-hub-item__nome:hover {
+ color: var(--accent);
+ text-decoration: underline;
+}
diff --git a/static/js/dashboard-contabil.js b/static/js/dashboard-contabil.js
index 5c8641e..065053a 100644
--- a/static/js/dashboard-contabil.js
+++ b/static/js/dashboard-contabil.js
@@ -70,6 +70,53 @@ function pidDcFormatMoeda(valor) {
return numero.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
}
+// Os 3 formatos abaixo espelham os filtros moeda/percentual/indice de
+// portal_api/templatetags/contabil_extras.py (mesmo relatório "Gerar
+// Dashboard") — diferente de pidDcFormatMoeda acima, `null`/`undefined`
+// sempre vira "—", nunca "R$ 0,00": em IndicadoresFinanceiros, `None`
+// significa "indisponível" (ex. EBITDA sem histórico), não zero.
+function pidDcFormatIndicadorMoeda(valor) {
+ if (valor === null || valor === undefined) return "—";
+ return Number(valor).toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
+}
+
+function pidDcFormatIndicadorPercentual(valor) {
+ if (valor === null || valor === undefined) return "—";
+ return `${(Number(valor) * 100).toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%`;
+}
+
+function pidDcFormatIndicadorIndice(valor) {
+ if (valor === null || valor === undefined) return "—";
+ return Number(valor).toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+}
+
+// Label/grupo/formato de cada indicador (de sistema ou personalizado) vêm
+// sempre do servidor agora (`GET /contabil-apuracoes/{id}/indicadores/`,
+// campo `metadados`) — única fonte de verdade, já que um indicador
+// personalizado não tem como ter seu formato hardcoded aqui. Só a ORDEM de
+// exibição dos grupos continua fixa no frontend (grupo desconhecido, se
+// algum dia existir, cai no fim).
+const PID_DC_DASH_ORDEM_GRUPOS = [
+ "Indicadores de Resultado",
+ "Indicadores de Liquidez e Endividamento",
+ "Indicadores Personalizados",
+];
+
+function pidDcFormatIndicadorGenerico(formato, valor) {
+ if (formato === "moeda") return pidDcFormatIndicadorMoeda(valor);
+ if (formato === "percentual") return pidDcFormatIndicadorPercentual(valor);
+ return pidDcFormatIndicadorIndice(valor);
+}
+
+const PID_DC_ICON_OLHO_ABERTO =
+ ' ';
+const PID_DC_ICON_OLHO_FECHADO =
+ ' ';
+const PID_DC_ICON_EDITAR =
+ ' ';
+const PID_DC_ICON_LIXEIRA =
+ ' ';
+
function pidDcFormatCompetencia(iso) {
if (!iso) return "";
const [ano, mes] = iso.split("-");
@@ -136,6 +183,68 @@ async function pidAtualizarAchadoContabil(id, status, observacaoContador) {
});
}
+// ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ----
+
+async function pidBuscarIndicadoresContabil(id) {
+ return pidApiRequest(`/contabil-apuracoes/${id}/indicadores/`);
+}
+
+async function pidAtualizarIndicadoresOcultosContabil(id, indicadoresOcultos) {
+ return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-ocultos/`, {
+ method: "POST",
+ body: { indicadores_ocultos: indicadoresOcultos },
+ });
+}
+
+async function pidAlternarOcultoContaContabil(id, oculto) {
+ return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: { oculta_no_relatorio: oculto } });
+}
+
+async function pidAlternarOcultoLinhaDreContabil(id, oculto) {
+ return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: { oculta_no_relatorio: oculto } });
+}
+
+async function pidAlternarOcultoAchadoContabil(id, oculto) {
+ return pidApiRequest(`/contabil-achados/${id}/alternar-oculto/`, {
+ method: "POST",
+ body: { oculto_no_relatorio: oculto },
+ });
+}
+
+async function pidBuscarIndicadorContabilDefinicao(id) {
+ return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`);
+}
+
+async function pidCriarIndicadorContabil(payload) {
+ return pidApiRequest("/contabil-indicadores-definicoes/", { method: "POST", body: payload });
+}
+
+async function pidAtualizarIndicadorContabil(id, payload) {
+ return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`, { method: "PATCH", body: payload });
+}
+
+async function pidExcluirIndicadorContabil(id) {
+ return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`, { method: "DELETE" });
+}
+
+async function pidBuscarDefinicoesIndicadoresContabil() {
+ return pidApiRequest("/contabil-indicadores-definicoes/");
+}
+
+async function pidPreVisualizarIndicadorContabil(apuracaoId, payload) {
+ return pidApiRequest(`/contabil-apuracoes/${apuracaoId}/pre-visualizar-indicador/`, {
+ method: "POST",
+ body: payload,
+ });
+}
+
+async function pidAtualizarIndicadoresSelecionadosContabil(id, indicadoresSelecionados) {
+ return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-selecionados/`, {
+ method: "POST",
+ body: { indicadores_selecionados: indicadoresSelecionados },
+ });
+}
+
document.addEventListener("DOMContentLoaded", async () => {
const listView = document.getElementById("dc-list-view");
if (!listView) return;
@@ -175,6 +284,10 @@ document.addEventListener("DOMContentLoaded", async () => {
// regras gerais, ex. balanceamento Ativo x Passivo, não têm uma conta
// única por trás, então não ganham o toggle).
let dcAchadosContaExpandida = new Set();
+ // { indicadores, indicadores_ocultos } da aba "Dashboard" — carregado sob
+ // demanda (só quando essa aba é aberta pela primeira vez após abrir/criar
+ // a apuração, ver renderDashboardTab), null até lá.
+ let dcIndicadoresAtual = null;
function showView(view) {
listView.hidden = view !== "list";
@@ -314,6 +427,7 @@ document.addEventListener("DOMContentLoaded", async () => {
dcContasColapsadas = new Set();
dcDreColapsadas = new Set();
dcAchadosContaExpandida = new Set();
+ dcIndicadoresAtual = null;
renderAchados();
renderContas();
renderDre();
@@ -691,7 +805,7 @@ document.addEventListener("DOMContentLoaded", async () => {
// ---- Tabs ----
- document.getElementById("dc-tabs").addEventListener("click", (event) => {
+ document.getElementById("dc-tabs").addEventListener("click", async (event) => {
const tabBtn = event.target.closest("[data-dc-tab]");
if (!tabBtn) return;
const tab = tabBtn.getAttribute("data-dc-tab");
@@ -699,6 +813,696 @@ document.addEventListener("DOMContentLoaded", async () => {
document.querySelectorAll("[data-dc-panel]").forEach((panel) => {
panel.hidden = panel.getAttribute("data-dc-panel") !== tab;
});
+ if (tab === "dashboard") await renderDashboardTab();
+ });
+
+ // ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ----
+
+ // Indicadores só são buscados na primeira vez que a aba é aberta (mesmo
+ // espírito de "canvas com tamanho zero não desenha certo" do relatório em
+ // si — aqui o motivo é só evitar um cálculo/consulta extra ao banco em
+ // toda apuração aberta, já que boa parte das revisões nem chega a olhar
+ // esta aba). Observações (contas/DRE/achados) já estão todas carregadas
+ // em apuracaoAtual, não precisam de fetch próprio.
+ async function renderDashboardTab() {
+ if (!dcIndicadoresAtual) {
+ dcIndicadoresAtual = await pidBuscarIndicadoresContabil(apuracaoAtual.id);
+ }
+ renderDashboardIndicadores();
+ renderDashboardObservacoes();
+ }
+
+ // Agrupa as chaves de `metadados` pelo campo `grupo` de cada uma (vem do
+ // servidor — os 11 de sistema já têm um grupo fixo, personalizados
+ // sempre caem em "Indicadores Personalizados", ver
+ // `ContabilApuracaoViewSet.indicadores()`); só a ORDEM de exibição dos
+ // grupos é decidida aqui (`PID_DC_DASH_ORDEM_GRUPOS`).
+ function renderDashboardIndicadores() {
+ const container = document.getElementById("dc-dash-indicadores");
+ container.innerHTML = "";
+ if (!dcIndicadoresAtual) return;
+ const { indicadores, indicadores_ocultos: indicadoresOcultos, metadados } = dcIndicadoresAtual;
+ const concluida = apuracaoAtual.status === "concluida";
+
+ const grupos = new Map();
+ Object.keys(metadados).forEach((chave) => {
+ const nomeGrupo = metadados[chave].grupo;
+ if (!grupos.has(nomeGrupo)) grupos.set(nomeGrupo, []);
+ grupos.get(nomeGrupo).push(chave);
+ });
+ const nomesGrupos = [...grupos.keys()].sort((a, b) => {
+ const posicaoA = PID_DC_DASH_ORDEM_GRUPOS.indexOf(a);
+ const posicaoB = PID_DC_DASH_ORDEM_GRUPOS.indexOf(b);
+ return (posicaoA === -1 ? 99 : posicaoA) - (posicaoB === -1 ? 99 : posicaoB);
+ });
+
+ nomesGrupos.forEach((nomeGrupo) => {
+ const secao = document.createElement("div");
+ secao.className = "dc-dash-grupo";
+ secao.innerHTML = `
+ ${pidDcEscapeHtml(nomeGrupo)}
+
+ `;
+ const grid = secao.querySelector(".dc-dash-cards-grid");
+ grupos.get(nomeGrupo).forEach((chave) => {
+ const meta = metadados[chave];
+ const oculto = indicadoresOcultos.includes(chave);
+ const card = document.createElement("div");
+ card.className = `dc-dash-card${oculto ? " is-oculto" : ""}`;
+ card.innerHTML = `
+
+ ${pidDcEscapeHtml(meta.nome)}
+ ${pidDcFormatIndicadorGenerico(meta.formato, indicadores[chave])}
+
+
+ ${PID_DC_ICON_EDITAR}
+ ${oculto ? PID_DC_ICON_OLHO_FECHADO : PID_DC_ICON_OLHO_ABERTO}
+
+ `;
+ card.addEventListener("click", (event) => {
+ if (event.target.closest("button")) return;
+ pidDcAbrirFormulaModal(meta);
+ });
+ grid.appendChild(card);
+ });
+ container.appendChild(secao);
+ });
+ }
+
+ document.getElementById("dc-dash-indicadores").addEventListener("click", async (event) => {
+ const editarBtn = event.target.closest("[data-dc-dash-indicador-editar]");
+ if (editarBtn) {
+ if (editarBtn.disabled) return;
+ const definicaoId = Number(editarBtn.getAttribute("data-dc-dash-indicador-editar"));
+ try {
+ const definicao = await pidBuscarIndicadorContabilDefinicao(definicaoId);
+ pidDcAbrirIndicadorModal(definicao);
+ } catch (e) {
+ await pidAlert(e.message);
+ }
+ return;
+ }
+ const btn = event.target.closest("[data-dc-dash-indicador-toggle]");
+ if (!btn || btn.disabled) return;
+ const chave = btn.getAttribute("data-dc-dash-indicador-toggle");
+ const atual = dcIndicadoresAtual.indicadores_ocultos;
+ const novaLista = atual.includes(chave) ? atual.filter((c) => c !== chave) : [...atual, chave];
+ btn.disabled = true;
+ try {
+ const resposta = await pidAtualizarIndicadoresOcultosContabil(apuracaoAtual.id, novaLista);
+ dcIndicadoresAtual.indicadores_ocultos = resposta.indicadores_ocultos;
+ renderDashboardIndicadores();
+ } catch (e) {
+ btn.disabled = false;
+ await pidAlert(e.message);
+ }
+ });
+
+ // ---- Modal "Ver fórmula" ----
+
+ // Aceita um objeto {nome, descricao, formula_texto} direto — não só uma
+ // chave de `dcIndicadoresAtual.metadados` (que só lista indicadores
+ // ATIVOS nesta apuração). Um indicador não padrão ainda não selecionado
+ // pra esta apuração não aparece em `metadados`, mas o hub de
+ // "Gerenciar Indicadores" (ver pidDcCriaLinhaHub) ainda precisa poder
+ // mostrar a fórmula dele — por isso o chamador monta o objeto certo em
+ // cada caso, em vez desta função sempre resolver por chave.
+ function pidDcAbrirFormulaModal(meta) {
+ if (!meta) return;
+ document.getElementById("dc-indicador-formula-modal-titulo").textContent = meta.nome;
+ document.getElementById("dc-indicador-formula-modal-descricao").textContent =
+ meta.descricao || "Sem descrição cadastrada.";
+ document.getElementById("dc-indicador-formula-modal-formula").textContent = meta.formula_texto || "—";
+ document.getElementById("dc-indicador-formula-modal").hidden = false;
+ }
+
+ document.getElementById("dc-indicador-formula-modal-fechar-btn").addEventListener("click", () => {
+ document.getElementById("dc-indicador-formula-modal").hidden = true;
+ });
+ document.getElementById("dc-indicador-formula-modal").addEventListener("click", (event) => {
+ if (event.target.id === "dc-indicador-formula-modal") {
+ document.getElementById("dc-indicador-formula-modal").hidden = true;
+ }
+ });
+
+ // ---- Modal "Novo/Editar Indicador" ----
+
+ const indicadorModal = document.getElementById("dc-indicador-modal");
+ const indicadorComponentesEl = document.getElementById("dc-indicador-componentes");
+ let dcIndicadorEmEdicao = null; // definição completa (id/chave/.../componentes) ou null = criando
+ let dcIndicadorComponenteSeq = 0;
+
+ const PID_DC_COMP_TIPO_LABELS = {
+ contas: "Soma de contas do Balancete",
+ linha_dre: "Soma de linhas da DRE",
+ variacao_conta: "Variação de contas (atual − anterior)",
+ indicador: "Referência a outro indicador",
+ resultado_liquido: "Resultado Líquido da DRE (última linha)",
+ };
+
+ function pidDcRenderComponentePicker(linhaEl, tipo, valoresIniciais) {
+ const picker = linhaEl.querySelector(".dc-ind-comp-picker");
+ if (tipo === "contas" || tipo === "variacao_conta") {
+ const selecionados = valoresIniciais.contas_codigos || [];
+ const codigosNestaApuracao = new Set(apuracaoAtual.contas.map((conta) => conta.codigo));
+ // Um indicador pode ter sido criado a partir de outra apuração
+ // (empresa/competência diferente) — código selecionado que não
+ // existe aqui ainda precisa continuar marcado e ser salvo de volta
+ // do jeito que estava, senão editar um indicador desses nesta tela
+ // apagaria silenciosamente a conta que só existe na apuração
+ // original. Aparece como item extra, sinalizado, no topo da lista.
+ const orfaos = selecionados.filter((codigo) => !codigosNestaApuracao.has(codigo));
+ const selecionadosSet = new Set(selecionados);
+ picker.innerHTML = `
+
+
+ ${orfaos.map((codigo) => `
+
+
+ ${pidDcEscapeHtml(codigo)} — conta não encontrada nesta apuração, mantida
+
+ `).join("")}
+ ${apuracaoAtual.contas.map((conta) => `
+
+
+ ${pidDcEscapeHtml(conta.codigo)} — ${pidDcEscapeHtml(conta.descricao)}
+
+ `).join("")}
+
+ `;
+ } else if (tipo === "linha_dre") {
+ const selecionados = valoresIniciais.linhas_dre_descricoes || [];
+ const descricoesNestaApuracao = new Set(apuracaoAtual.linhas_dre.map((linha) => linha.descricao));
+ const orfaos = selecionados.filter((descricao) => !descricoesNestaApuracao.has(descricao));
+ const selecionadosSet = new Set(selecionados);
+ picker.innerHTML = `
+
+
+ ${orfaos.map((descricao) => `
+
+
+ ${pidDcEscapeHtml(descricao)} — linha não encontrada nesta apuração, mantida
+
+ `).join("")}
+ ${apuracaoAtual.linhas_dre.map((linha) => `
+
+
+ ${pidDcEscapeHtml(linha.descricao)}
+
+ `).join("")}
+
+ `;
+ } else if (tipo === "indicador") {
+ const atual = valoresIniciais.indicador_referenciado || "";
+ const chaveEditando = dcIndicadorEmEdicao ? dcIndicadorEmEdicao.chave : null;
+ const metadados = (dcIndicadoresAtual && dcIndicadoresAtual.metadados) || {};
+ const opcoes = Object.keys(metadados)
+ .filter((chave) => chave !== chaveEditando)
+ .sort((a, b) => metadados[a].nome.localeCompare(metadados[b].nome));
+ picker.innerHTML = `
+
+ Selecione um indicador...
+ ${opcoes.map((chave) => `${pidDcEscapeHtml(metadados[chave].nome)} `).join("")}
+
+ `;
+ } else if (tipo === "resultado_liquido") {
+ // Sem seleção nenhuma — sempre resolve pra última linha da DRE desta
+ // apuração, seja qual for o rótulo (evita quebrar quando o resultado
+ // muda de sinal e a linha passa de "LUCRO" pra "PREJUÍZO", ou
+ // vice-versa, ver dashboard_contabil/models.py).
+ picker.innerHTML = `Usa automaticamente o resultado líquido do período (última linha da DRE desta apuração), sem precisar escolher nada aqui.
`;
+ } else {
+ picker.innerHTML = "";
+ }
+ }
+
+ function pidDcCriaLinhaComponente(valoresIniciais) {
+ dcIndicadorComponenteSeq += 1;
+ const linha = document.createElement("div");
+ linha.className = "dc-ind-componente";
+ linha.dataset.dcCompId = String(dcIndicadorComponenteSeq);
+ const tipoInicial = valoresIniciais.tipo || "contas";
+ linha.innerHTML = `
+
+
+ Chave (usada na fórmula)
+
+
+
+ Tipo
+
+ ${Object.keys(PID_DC_COMP_TIPO_LABELS).map((tipo) => `${PID_DC_COMP_TIPO_LABELS[tipo]} `).join("")}
+
+
+
${PID_DC_ICON_LIXEIRA}
+
+
+ `;
+ pidDcRenderComponentePicker(linha, tipoInicial, valoresIniciais);
+ return linha;
+ }
+
+ indicadorComponentesEl.addEventListener("change", (event) => {
+ if (event.target.classList.contains("dc-ind-comp-tipo")) {
+ const linha = event.target.closest(".dc-ind-componente");
+ pidDcRenderComponentePicker(linha, event.target.value, {});
+ }
+ });
+ indicadorComponentesEl.addEventListener("input", (event) => {
+ if (!event.target.hasAttribute("data-dc-comp-busca")) return;
+ const linha = event.target.closest(".dc-ind-componente");
+ const termo = event.target.value.trim().toLowerCase();
+ linha.querySelectorAll("[data-dc-comp-item]").forEach((item) => {
+ item.hidden = termo !== "" && !item.getAttribute("data-dc-comp-item").includes(termo);
+ });
+ });
+ indicadorComponentesEl.addEventListener("click", (event) => {
+ const removerBtn = event.target.closest(".dc-ind-comp-remover");
+ if (removerBtn) removerBtn.closest(".dc-ind-componente").remove();
+ });
+
+ document.getElementById("dc-indicador-add-componente-btn").addEventListener("click", () => {
+ indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente({}));
+ });
+
+ function pidDcAbrirIndicadorModal(definicao) {
+ dcIndicadorEmEdicao = definicao || null;
+ document.getElementById("dc-indicador-modal-titulo").textContent = definicao ? "Editar Indicador" : "Novo Indicador";
+ document.getElementById("dc-indicador-modal-nome").value = definicao ? definicao.nome : "";
+ document.getElementById("dc-indicador-modal-descricao").value = definicao ? definicao.descricao : "";
+ document.getElementById("dc-indicador-modal-formato").value = definicao ? definicao.formato : "indice";
+ document.getElementById("dc-indicador-modal-formula").value = definicao ? definicao.formula : "";
+ document.getElementById("dc-indicador-modal-padrao").checked = definicao ? definicao.padrao : true;
+ document.getElementById("dc-indicador-modal-error").textContent = "";
+ document.getElementById("dc-indicador-modal-excluir-btn").hidden = !definicao;
+ document.getElementById("dc-indicador-preview").hidden = true;
+
+ indicadorComponentesEl.innerHTML = "";
+ const componentesIniciais = definicao ? definicao.componentes : [];
+ if (componentesIniciais.length) {
+ componentesIniciais.forEach((componente) => indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente(componente)));
+ } else {
+ indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente({}));
+ }
+ indicadorModal.hidden = false;
+ }
+
+ function pidDcFecharIndicadorModal() {
+ indicadorModal.hidden = true;
+ dcIndicadorEmEdicao = null;
+ }
+
+ document.getElementById("dc-indicador-modal-cancel-btn").addEventListener("click", pidDcFecharIndicadorModal);
+ indicadorModal.addEventListener("click", (event) => {
+ if (event.target === indicadorModal) pidDcFecharIndicadorModal();
+ });
+
+ // Depois de criar/editar/excluir, além de recarregar os cards da aba
+ // "Dashboard", atualiza o hub também se ele estiver aberto por trás
+ // (o form de indicador é sempre stacked por cima dele, ver
+ // `.modal-overlay--top`/pidDcAbrirHubIndicadores) — sem isso, o hub
+ // ficaria com a lista desatualizada até ser reaberto.
+ async function pidDcAposSalvarOuExcluirIndicador() {
+ dcIndicadoresAtual = null;
+ await renderDashboardTab();
+ if (!indicadoresHubModal.hidden) await pidDcRenderHubIndicadores();
+ }
+
+ // Reaproveitada pelo "Salvar" e pelo "Calcular com esta apuração" — os
+ // dois precisam exatamente da mesma leitura do formulário (chave/tipo/
+ // seleção de cada linha de componente).
+ function pidDcColetaComponentesForm() {
+ return [...indicadorComponentesEl.querySelectorAll(".dc-ind-componente")].map((linha) => {
+ const tipo = linha.querySelector(".dc-ind-comp-tipo").value;
+ const chave = linha.querySelector(".dc-ind-comp-chave").value.trim();
+ const componente = { chave, tipo, contas_codigos: [], linhas_dre_descricoes: [], indicador_referenciado: "" };
+ if (tipo === "contas" || tipo === "variacao_conta") {
+ componente.contas_codigos = [...linha.querySelectorAll('[data-dc-comp-lista] input[type="checkbox"]:checked')].map(
+ (cb) => cb.value
+ );
+ } else if (tipo === "linha_dre") {
+ componente.linhas_dre_descricoes = [
+ ...linha.querySelectorAll('[data-dc-comp-lista] input[type="checkbox"]:checked'),
+ ].map((cb) => cb.value);
+ } else if (tipo === "indicador") {
+ const select = linha.querySelector(".dc-ind-comp-indicador");
+ componente.indicador_referenciado = select ? select.value : "";
+ }
+ return componente;
+ });
+ }
+
+ document.getElementById("dc-indicador-modal-save-btn").addEventListener("click", async () => {
+ const errorEl = document.getElementById("dc-indicador-modal-error");
+ errorEl.textContent = "";
+
+ const componentes = pidDcColetaComponentesForm();
+
+ const payload = {
+ nome: document.getElementById("dc-indicador-modal-nome").value.trim(),
+ descricao: document.getElementById("dc-indicador-modal-descricao").value.trim(),
+ formato: document.getElementById("dc-indicador-modal-formato").value,
+ formula: document.getElementById("dc-indicador-modal-formula").value.trim(),
+ padrao: document.getElementById("dc-indicador-modal-padrao").checked,
+ componentes,
+ };
+
+ if (!payload.nome) {
+ errorEl.textContent = "Informe o nome do indicador.";
+ return;
+ }
+ if (!componentes.length) {
+ errorEl.textContent = "Adicione pelo menos um componente.";
+ return;
+ }
+ if (componentes.some((c) => !c.chave)) {
+ errorEl.textContent = "Preencha a chave de todos os componentes.";
+ return;
+ }
+ if (!payload.formula) {
+ errorEl.textContent = "Informe a fórmula do indicador.";
+ return;
+ }
+
+ const saveBtn = document.getElementById("dc-indicador-modal-save-btn");
+ saveBtn.disabled = true;
+ try {
+ if (dcIndicadorEmEdicao) {
+ await pidAtualizarIndicadorContabil(dcIndicadorEmEdicao.id, payload);
+ } else {
+ await pidCriarIndicadorContabil(payload);
+ }
+ pidDcFecharIndicadorModal();
+ await pidDcAposSalvarOuExcluirIndicador();
+ } catch (e) {
+ errorEl.textContent = e.message;
+ } finally {
+ saveBtn.disabled = false;
+ }
+ });
+
+ // Calcula, contra a apuração aberta agora, o valor de cada componente e
+ // o resultado final — sem salvar nada — pro contador conferir se a
+ // fórmula pegou a conta/linha certa antes de confirmar. Reaproveita a
+ // mesma coleta do formulário do botão "Salvar" (`pidDcColetaComponentesForm`).
+ document.getElementById("dc-indicador-calcular-btn").addEventListener("click", async () => {
+ const previewEl = document.getElementById("dc-indicador-preview");
+ const componentes = pidDcColetaComponentesForm();
+ const formula = document.getElementById("dc-indicador-modal-formula").value.trim();
+
+ previewEl.hidden = false;
+ if (!componentes.length || componentes.some((c) => !c.chave) || !formula) {
+ previewEl.innerHTML = `Preencha a chave de todos os componentes e a fórmula antes de calcular.
`;
+ return;
+ }
+
+ const payload = {
+ nome: document.getElementById("dc-indicador-modal-nome").value.trim() || "Pré-visualização",
+ descricao: "",
+ formato: document.getElementById("dc-indicador-modal-formato").value,
+ formula,
+ componentes,
+ };
+
+ const calcularBtn = document.getElementById("dc-indicador-calcular-btn");
+ calcularBtn.disabled = true;
+ previewEl.innerHTML = `Calculando...
`;
+ try {
+ const resultado = await pidPreVisualizarIndicadorContabil(apuracaoAtual.id, payload);
+ if (resultado.erro) {
+ previewEl.innerHTML = `${pidDcEscapeHtml(resultado.erro)}
`;
+ } else {
+ previewEl.innerHTML = `
+
+ ${resultado.componentes.map((c) => `
+ ${pidDcEscapeHtml(c.chave)} ${c.valor_formatado === null ? "—" : pidDcEscapeHtml(c.valor_formatado)}
+ `).join("")}
+
+ Resultado: ${resultado.resultado_formatado === null ? "—" : pidDcEscapeHtml(resultado.resultado_formatado)}
+ `;
+ }
+ } catch (e) {
+ previewEl.innerHTML = `${pidDcEscapeHtml(e.message)}
`;
+ } finally {
+ calcularBtn.disabled = false;
+ }
+ });
+
+ document.getElementById("dc-indicador-modal-excluir-btn").addEventListener("click", async () => {
+ if (!dcIndicadorEmEdicao) return;
+ if (
+ !(await pidConfirm(`Excluir o indicador "${dcIndicadorEmEdicao.nome}"? Essa ação não pode ser desfeita.`, {
+ perigoso: true,
+ }))
+ ) {
+ return;
+ }
+ const errorEl = document.getElementById("dc-indicador-modal-error");
+ errorEl.textContent = "";
+ try {
+ await pidExcluirIndicadorContabil(dcIndicadorEmEdicao.id);
+ pidDcFecharIndicadorModal();
+ await pidDcAposSalvarOuExcluirIndicador();
+ } catch (e) {
+ errorEl.textContent = e.message;
+ }
+ });
+
+ // ---- Modal "Gerenciar Indicadores" (hub: lista todos os indicadores —
+ // padrão e não padrão — pra visualizar/editar/ativar, mesmo os que não
+ // estão aparecendo nesta apuração agora) ----
+
+ const indicadoresHubModal = document.getElementById("dc-indicadores-hub-modal");
+
+ function pidDcCriaLinhaHub(definicao, ativo, concluida) {
+ const linha = document.createElement("div");
+ linha.className = "dc-ind-hub-item";
+ linha.innerHTML = `
+ ${
+ ativo === null
+ ? ` `
+ : ` `
+ }
+ ${pidDcEscapeHtml(definicao.nome)}
+ ${PID_DC_ICON_EDITAR}
+ `;
+ return linha;
+ }
+
+ async function pidDcRenderHubIndicadores() {
+ const listaPadrao = document.getElementById("dc-ind-hub-lista-padrao");
+ const listaNaoPadrao = document.getElementById("dc-ind-hub-lista-nao-padrao");
+ const vazioPadrao = document.getElementById("dc-ind-hub-vazio-padrao");
+ const vazioNaoPadrao = document.getElementById("dc-ind-hub-vazio-nao-padrao");
+ listaPadrao.innerHTML = "";
+ listaNaoPadrao.innerHTML = "";
+
+ let definicoes;
+ try {
+ definicoes = await pidBuscarDefinicoesIndicadoresContabil();
+ } catch (e) {
+ await pidAlert(e.message);
+ return;
+ }
+
+ const selecionados = new Set((dcIndicadoresAtual && dcIndicadoresAtual.indicadores_selecionados) || []);
+ const concluida = apuracaoAtual.status === "concluida";
+ const padrao = definicoes.filter((d) => d.padrao);
+ const naoPadrao = definicoes.filter((d) => !d.padrao);
+
+ vazioPadrao.hidden = padrao.length > 0;
+ vazioNaoPadrao.hidden = naoPadrao.length > 0;
+ padrao.forEach((definicao) => listaPadrao.appendChild(pidDcCriaLinhaHub(definicao, null, concluida)));
+ naoPadrao.forEach((definicao) =>
+ listaNaoPadrao.appendChild(pidDcCriaLinhaHub(definicao, selecionados.has(definicao.chave), concluida))
+ );
+ }
+
+ async function pidDcAbrirHubIndicadores() {
+ indicadoresHubModal.hidden = false;
+ await pidDcRenderHubIndicadores();
+ }
+
+ document.getElementById("dc-indicadores-gerenciar-btn").addEventListener("click", pidDcAbrirHubIndicadores);
+ document.getElementById("dc-indicadores-hub-fechar-btn").addEventListener("click", () => {
+ indicadoresHubModal.hidden = true;
+ });
+ indicadoresHubModal.addEventListener("click", (event) => {
+ if (event.target === indicadoresHubModal) indicadoresHubModal.hidden = true;
+ });
+ document.getElementById("dc-indicadores-hub-novo-btn").addEventListener("click", () => pidDcAbrirIndicadorModal(null));
+
+ indicadoresHubModal.addEventListener("click", async (event) => {
+ const verBtn = event.target.closest("[data-dc-hub-ver]");
+ if (verBtn) {
+ const definicaoId = Number(verBtn.getAttribute("data-dc-hub-ver"));
+ try {
+ const definicao = await pidBuscarIndicadorContabilDefinicao(definicaoId);
+ pidDcAbrirFormulaModal({
+ nome: definicao.nome,
+ descricao: definicao.descricao,
+ formula_texto: definicao.formula,
+ });
+ } catch (e) {
+ await pidAlert(e.message);
+ }
+ return;
+ }
+ const editarBtn = event.target.closest("[data-dc-hub-editar]");
+ if (editarBtn) {
+ if (editarBtn.disabled) return;
+ const definicaoId = Number(editarBtn.getAttribute("data-dc-hub-editar"));
+ try {
+ const definicao = await pidBuscarIndicadorContabilDefinicao(definicaoId);
+ pidDcAbrirIndicadorModal(definicao);
+ } catch (e) {
+ await pidAlert(e.message);
+ }
+ }
+ });
+
+ indicadoresHubModal.addEventListener("change", async (event) => {
+ const checkbox = event.target.closest("[data-dc-hub-toggle]");
+ if (!checkbox || checkbox.disabled) return;
+ const chave = checkbox.getAttribute("data-dc-hub-toggle");
+ const atual = new Set(dcIndicadoresAtual.indicadores_selecionados || []);
+ if (checkbox.checked) atual.add(chave);
+ else atual.delete(chave);
+ checkbox.disabled = true;
+ try {
+ await pidAtualizarIndicadoresSelecionadosContabil(apuracaoAtual.id, [...atual]);
+ dcIndicadoresAtual = null;
+ await renderDashboardTab();
+ await pidDcRenderHubIndicadores();
+ } catch (e) {
+ checkbox.checked = !checkbox.checked;
+ checkbox.disabled = false;
+ await pidAlert(e.message);
+ }
+ });
+
+ function renderDashboardObservacoes() {
+ const lista = document.getElementById("dc-dash-observacoes-list");
+ const empty = document.getElementById("dc-dash-observacoes-empty");
+ const concluida = apuracaoAtual.status === "concluida";
+ lista.innerHTML = "";
+
+ const itens = [
+ ...apuracaoAtual.contas
+ .filter((conta) => conta.observacao)
+ .map((conta) => ({
+ tipo: "conta",
+ id: conta.id,
+ origem: "Balancete",
+ referencia: `${conta.codigo} — ${conta.descricao}`,
+ texto: conta.observacao,
+ oculto: conta.oculta_no_relatorio,
+ })),
+ ...apuracaoAtual.linhas_dre
+ .filter((linha) => linha.observacao)
+ .map((linha) => ({
+ tipo: "dre",
+ id: linha.id,
+ origem: "D.R.E.",
+ referencia: linha.descricao,
+ texto: linha.observacao,
+ oculto: linha.oculta_no_relatorio,
+ })),
+ ...apuracaoAtual.achados
+ .filter((achado) => achado.observacao_contador)
+ .map((achado) => ({
+ tipo: "achado",
+ id: achado.id,
+ origem: "Auditoria",
+ referencia: achado.titulo,
+ texto: achado.observacao_contador,
+ oculto: achado.oculto_no_relatorio,
+ })),
+ ];
+
+ empty.hidden = itens.length > 0;
+
+ itens.forEach((item) => {
+ const linha = document.createElement("div");
+ linha.className = `dc-dash-obs${item.oculto ? " is-oculto" : ""}`;
+ linha.innerHTML = `
+
+
${pidDcEscapeHtml(item.origem)}
+
${pidDcEscapeHtml(item.referencia)}
+
${pidDcEscapeHtml(item.texto)}
+
+
+ ${item.oculto ? PID_DC_ICON_OLHO_FECHADO : PID_DC_ICON_OLHO_ABERTO}
+
+ `;
+ lista.appendChild(linha);
+ });
+ }
+
+ document.getElementById("dc-dash-observacoes-list").addEventListener("click", async (event) => {
+ const btn = event.target.closest("[data-dc-dash-obs-toggle]");
+ if (!btn || btn.disabled) return;
+ const [tipo, idTexto] = btn.getAttribute("data-dc-dash-obs-toggle").split(":");
+ const id = Number(idTexto);
+ btn.disabled = true;
+ try {
+ if (tipo === "conta") {
+ const conta = apuracaoAtual.contas.find((c) => c.id === id);
+ const atualizada = await pidAlternarOcultoContaContabil(id, !conta.oculta_no_relatorio);
+ const indice = apuracaoAtual.contas.findIndex((c) => c.id === id);
+ apuracaoAtual.contas[indice] = atualizada;
+ } else if (tipo === "dre") {
+ const linhaDre = apuracaoAtual.linhas_dre.find((l) => l.id === id);
+ const atualizada = await pidAlternarOcultoLinhaDreContabil(id, !linhaDre.oculta_no_relatorio);
+ const indice = apuracaoAtual.linhas_dre.findIndex((l) => l.id === id);
+ apuracaoAtual.linhas_dre[indice] = atualizada;
+ } else {
+ const achado = apuracaoAtual.achados.find((a) => a.id === id);
+ const atualizado = await pidAlternarOcultoAchadoContabil(id, !achado.oculto_no_relatorio);
+ const indice = apuracaoAtual.achados.findIndex((a) => a.id === id);
+ apuracaoAtual.achados[indice] = atualizado;
+ }
+ renderDashboardObservacoes();
+ } catch (e) {
+ btn.disabled = false;
+ await pidAlert(e.message);
+ }
});
// ---- Modal de observação (reaproveitado por conta do Balancete e linha da DRE) ----
diff --git a/templates/dashboard-contabil-relatorio.html b/templates/dashboard-contabil-relatorio.html
index 239359c..8f6201e 100644
--- a/templates/dashboard-contabil-relatorio.html
+++ b/templates/dashboard-contabil-relatorio.html
@@ -527,77 +527,28 @@
+ {% for grupo in indicadores_grupos %}
-
- Indicadores de Resultado
+
+ {{ grupo.nome }}
-
-
-
ROA
-
{{ indicadores.roa|percentual }}
-
-
-
-
ROE
-
{{ indicadores.roe|percentual }}
-
-
-
-
Kanitz
-
{{ indicadores.kanitz|indice }}
-
Estimativa — fórmula padrão, não calibrada
-
-
-
-
EBIT
-
{{ indicadores.ebit|moeda }}
-
-
-
-
EBITDA
-
{{ indicadores.ebitda|moeda }}
- {% if indicadores.ebitda is None %}
Sem apuração anterior desta empresa
{% endif %}
-
-
-
-
-
- Indicadores de Liquidez e Endividamento
-
-
-
-
-
Liquidez Corrente
-
{{ indicadores.liquidez_corrente|indice }}
-
-
-
-
Liquidez Seca
-
{{ indicadores.liquidez_seca|indice }}
-
-
-
-
Liquidez Geral
-
{{ indicadores.liquidez_geral|indice }}
-
Aproximado — ver observação no relatório técnico
-
-
-
-
Composição do Endividamento
-
{{ indicadores.composicao_endividamento|percentual }}
-
-
-
-
Grau de Endividamento
-
{{ indicadores.grau_endividamento|percentual }}
-
-
-
-
IPL
-
{{ indicadores.ipl|percentual }}
+ {% for indicador in grupo.cards %}
+
+
+
{{ indicador.nome }}
+
{{ indicador.valor_formatado }}
+ {% if indicador.nota %}
{{ indicador.nota }}
{% endif %}
+ {% endfor %}
+ {% endfor %}
diff --git a/templates/dashboard-contabil.html b/templates/dashboard-contabil.html
index c0fc115..f6051e7 100644
--- a/templates/dashboard-contabil.html
+++ b/templates/dashboard-contabil.html
@@ -406,6 +406,7 @@
Observações
Balancete
DRE
+ Dashboard
@@ -476,6 +477,27 @@
+
+
+
+ Pré-visualização do que vai para o relatório gerado pelo botão "Gerar Dashboard". Use o botão de olho pra ocultar um card de indicador ou uma observação — o que estiver oculto aqui não aparece no relatório.
+
+
+
+
+ Clique num card pra ver a fórmula usada.
+
+
+
+
+ Observações
+
+ Nenhuma observação registrada ainda no Balancete, na DRE ou na auditoria.
+
+
@@ -514,6 +536,97 @@
+
+
+
+
+
Gerenciar Indicadores
+
Indicadores padrão aparecem automaticamente em toda apuração. Indicadores não padrão ficam salvos aqui e só entram nesta apuração quando marcados.
+
+
+
Padrão
+
+
Nenhum indicador padrão personalizado cadastrado.
+
+
+
+
Não padrão
+
+
Nenhum indicador não padrão cadastrado.
+
+
+
+ Fechar
+ + Novo Indicador
+
+
+
+
+
+
+
Novo Indicador
+
Monte a fórmula a partir de contas do Balancete, linhas da DRE, variação entre apurações ou outros indicadores já existentes.
+
+
+ Nome
+
+
+
+
+ Descrição
+
+
+
+ Formato de exibição
+
+ Índice
+ Percentual (%)
+ Moeda (R$)
+
+
+
+
+
+
+ Indicador padrão (aparece automaticamente em todas as apurações — desmarque pra deixar disponível só quando selecionado em "Gerenciar Indicadores")
+
+
+
+
+
+
+
Fórmula
+
+
Use a chave de cada componente acima, combinadas com +, -, * , / e parênteses.
+
+
+
Calcular com esta apuração
+
+
+
+
+ Excluir
+ Cancelar
+ Salvar
+
+
+
+