const PID_DC_STATUS_LABELS = { revisao: "Em revisão", concluida: "Concluída" };
const PID_DC_SEVERIDADE_LABELS = { alta: "Alta", media: "Média", baixa: "Baixa" };
const PID_DC_ACHADO_STATUS_LABELS = { pendente: "Pendente", tratado: "Tratado", ignorado: "Ignorado" };
// Ordem de exibição do donut/legenda de severidade no resumo — mesmas cores
// já usadas pelos badges (.dc-badge--alta/--media/--baixa, dashboard-contabil.css).
const PID_DC_SEVERIDADE_LISTA = ["alta", "media", "baixa"];
const PID_DC_SEVERIDADE_CORES = { alta: "var(--danger)", media: "var(--gold)", baixa: "rgb(var(--slate-rgb))" };
// Mesmas 10 regras/chaves de regras.py (REGRAS) — enumeradas aqui pra sempre
// mostrar as 10 categorias no resumo, mesmo as que não geraram achado nesta
// apuração (mesmo espírito do "Nenhum registro encontrado" do sistema externo
// que inspirou esta visão).
const PID_DC_REGRAS = [
{ chave: "balanceamento_ativo_passivo", label: "Balanceamento Ativo x Passivo" },
{ chave: "debito_credito_divergente", label: "Débito x Crédito" },
{ chave: "saldo_negativo_caixa", label: "Caixa Negativo" },
{ chave: "conta_transitoria_com_saldo", label: "Contas Transitórias" },
{ chave: "conta_deveria_zerar", label: "Contas que Deveriam Zerar" },
{ chave: "saldo_sinal_invertido", label: "Sinal de Saldo Invertido" },
{ chave: "descricao_generica", label: "Descrição Genérica" },
{ chave: "variacao_atipica_saldo", label: "Variação Atípica de Saldo" },
{ chave: "variacao_atipica_dre", label: "Variação Atípica na DRE" },
{ chave: "percentual_custo_receita_atipico", label: "Percentual Custo/Receita" },
];
// Agrupamento temático das 10 regras acima pro resumo da aba Observações —
// evita 10 cards soltos no grid (poluído visualmente com uma tabela por
// conta em cada um); cada grupo vira 1 card com as regras dele listadas por
// linha (label + contagem), clique numa linha com contagem > 0 continua
// isolando achado.regra na lista abaixo, igual antes.
const PID_DC_GRUPOS = [
{
label: "Divergências de Saldo",
regras: [
"balanceamento_ativo_passivo",
"debito_credito_divergente",
"saldo_negativo_caixa",
"saldo_sinal_invertido",
],
},
{
label: "Contas Atípicas",
regras: ["conta_transitoria_com_saldo", "conta_deveria_zerar", "descricao_generica"],
},
{
label: "Variações e Indicadores",
regras: ["variacao_atipica_saldo", "variacao_atipica_dre", "percentual_custo_receita_atipico"],
},
];
// Paleta de 10 cores distintas reaproveitando os tokens *-rgb já existentes
// (theme-aware — acompanham o tema claro/escuro e a cor de tema escolhida
// pelo usuário), com color-mix() pra completar até 10 sem hardcoded hex novo.
const PID_DC_CATEGORIA_CORES = [
"rgb(var(--accent-rgb))",
"rgb(var(--danger-rgb))",
"rgb(var(--gold-rgb))",
"rgb(var(--teal-rgb))",
"rgb(var(--slate-rgb))",
"rgb(var(--coral-rgb))",
"color-mix(in srgb, rgb(var(--accent-rgb)) 55%, white)",
"color-mix(in srgb, rgb(var(--danger-rgb)) 55%, white)",
"color-mix(in srgb, rgb(var(--gold-rgb)) 55%, black)",
"color-mix(in srgb, rgb(var(--teal-rgb)) 55%, black)",
];
function pidDcFormatMoeda(valor) {
const numero = Number(valor || 0);
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 =
'';
// Percentual da Análise Vertical já vem "pronto" do PDF (ex. "-7.33" quer
// dizer -7,33%, não -733%) — diferente de pidDcFormatIndicadorPercentual
// (usado pelos cards de indicador), que recebe uma fração e multiplica por
// 100 antes de exibir. Reusar aquele aqui exibiria "-733,00%" errado.
function pidDcFormatPercentualAnaliseVertical(valor) {
const numero = Number(valor || 0);
return `${numero.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%`;
}
// "mai/2026" (formato salvo pelo parser) -> "Mai/2026" (rótulo de coluna).
function pidDcFormatMesAnaliseVertical(mes) {
if (!mes) return "";
return mes.charAt(0).toUpperCase() + mes.slice(1);
}
function pidDcFormatCompetencia(iso) {
if (!iso) return "";
const [ano, mes] = iso.split("-");
return `${mes}/${ano}`;
}
function pidDcFormatData(iso) {
if (!iso) return "";
return new Date(iso).toLocaleDateString("pt-BR");
}
function pidDcEscapeHtml(texto) {
const div = document.createElement("div");
div.textContent = texto == null ? "" : String(texto);
return div.innerHTML;
}
async function pidFetchApuracoesContabil() {
return pidApiRequest("/contabil-apuracoes/");
}
async function pidFetchApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/`);
}
async function pidCriarApuracaoContabil(formData) {
return pidApiRequest("/contabil-apuracoes/", { method: "POST", body: formData });
}
async function pidExcluirApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/`, { method: "DELETE" });
}
async function pidConcluirApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/concluir/`, { method: "POST" });
}
/**
* /dashboard/ é GET (não POST, diferente do padrão /gerar/ de outras
* ferramentas) exatamente pra poder ser aberto assim — navegação de
* verdade numa nova aba, não um blob em memória. Um blob criado via fetch
* (`URL.createObjectURL`) tem sua própria origem sintética: URLs relativas
* dentro do HTML (como `{% static %}` da logo do escritório, que gera
* "/static/...") não resolvem de forma confiável contra a origem do Portal
* dentro de um documento carregado de um `blob:`, quebrando a imagem — com
* navegação de verdade pra URL da API, isso deixa de ser um problema.
*/
function pidGerarDashboardContabil(id) {
window.open(`/api/contabil-apuracoes/${id}/dashboard/`, "_blank");
}
// `payload` sempre {observacao, oculta_no_relatorio} — o editor inline
// (ver renderContas/renderDre) salva os dois campos juntos numa única
// chamada, já que "mostrar ao cliente" é decidido no mesmo instante em que
// a observação é escrita.
async function pidAtualizarObservacaoContaContabil(id, payload) {
return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: payload });
}
async function pidAtualizarObservacaoLinhaDreContabil(id, payload) {
return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: payload });
}
async function pidAtualizarValidadoContaContabil(id, validado) {
return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: { validado } });
}
async function pidAtualizarValidadoLinhaDreContabil(id, validado) {
return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: { validado } });
}
async function pidAtualizarObservacaoLinhaAnaliseVerticalContabil(id, payload) {
return pidApiRequest(`/contabil-linhas-analise-vertical/${id}/`, { method: "PATCH", body: payload });
}
async function pidAtualizarValidadoLinhaAnaliseVerticalContabil(id, validado) {
return pidApiRequest(`/contabil-linhas-analise-vertical/${id}/`, { method: "PATCH", body: { validado } });
}
async function pidAtualizarAchadoContabil(id, status, observacaoContador) {
return pidApiRequest(`/contabil-achados/${id}/`, {
method: "PATCH",
body: { status, observacao_contador: 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 pidAlternarOcultoLinhaAnaliseVerticalContabil(id, oculto) {
return pidApiRequest(`/contabil-linhas-analise-vertical/${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 },
});
}
async function pidAtualizarResumoFechamentoContabil(id, resumoFechamento) {
return pidApiRequest(`/contabil-apuracoes/${id}/resumo-fechamento/`, {
method: "POST",
body: { resumo_fechamento: resumoFechamento },
});
}
document.addEventListener("DOMContentLoaded", async () => {
const listView = document.getElementById("dc-list-view");
if (!listView) return;
const me = await pidGetMe();
if (!me) return;
const relatoriosPerms = (me.permissoes_efetivas && me.permissoes_efetivas["relatorios"]) || {};
const canUse = !!(relatoriosPerms.enabled && relatoriosPerms.apps && relatoriosPerms.apps["dashboard-contabil"]);
const noAccessEl = document.getElementById("dc-no-access");
const contentEl = document.getElementById("dc-content");
if (noAccessEl) noAccessEl.hidden = canUse;
if (contentEl) contentEl.hidden = !canUse;
if (!canUse) return;
const formView = document.getElementById("dc-form-view");
const reviewView = document.getElementById("dc-review-view");
let apuracaoAtual = null;
let filtroSeveridade = "todas";
let filtroStatusAchado = "pendente";
// Categoria (achado.regra) selecionada clicando num card do resumo — null
// = nenhuma categoria isolada. Filtro independente de severidade/status,
// os três se combinam (E lógico) na lista detalhada abaixo do resumo.
let filtroRegra = null;
let achadoEmEdicao = null;
// Ids de ContabilConta com filhos recolhidos na árvore do Balancete —
// reiniciado toda vez que uma apuração é aberta/criada (ver renderRevisao).
let dcContasColapsadas = new Set();
// Mesma ideia, mas para a árvore da DRE (ContabilLinhaDre.id) — nível de
// indentação já vem pronto do backend (linha.nivel), sem precisar derivar
// de um código de classificação como no Balancete.
let dcDreColapsadas = new Set();
// Id da conta/linha com o editor inline de observação aberto (no máximo um
// por tabela) — substitui o antigo modal `#dc-observacao-modal`: clicar no
// botão de observação abre uma linha extra logo abaixo da conta/linha
// dentro da própria tabela, evitando perder o texto por causa de um clique
// fora do popup (pedido explícito do usuário). null = nenhum editor aberto.
let dcContaObsEditId = null;
let dcDreObsEditId = null;
// Mesma ideia, agora pra árvore da Análise Vertical (ContabilLinhaAnaliseVertical)
// — mesmo nível de indentação da DRE (linha.nivel), só que cada linha tem
// um valor+percentual por mês em vez de um valor único.
let dcAvColapsadas = new Set();
let dcAvObsEditId = null;
// Ids de ContabilAchado com o detalhe da conta usada no apontamento
// expandido (ver renderAchados) — só achados com `conta` preenchido (as
// 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";
formView.hidden = view !== "form";
reviewView.hidden = view !== "review";
}
// ---- Histórico ----
async function carregarLista() {
const apuracoes = await pidFetchApuracoesContabil();
const body = document.getElementById("dc-list-body");
const empty = document.getElementById("dc-list-empty");
body.innerHTML = "";
empty.hidden = apuracoes.length > 0;
apuracoes.forEach((apuracao) => {
const tr = document.createElement("tr");
tr.innerHTML = `
`;
lista.appendChild(card);
});
}
document.getElementById("dc-achados-list").addEventListener("click", (event) => {
const toggleBtn = event.target.closest("[data-dc-achado-toggle-conta]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-achado-toggle-conta"));
if (dcAchadosContaExpandida.has(id)) dcAchadosContaExpandida.delete(id);
else dcAchadosContaExpandida.add(id);
renderAchados();
return;
}
const btn = event.target.closest("[data-dc-achado-editar]");
if (!btn) return;
abrirModalAchado(Number(btn.getAttribute("data-dc-achado-editar")));
});
// Reaproveitado tanto pelos chips de severidade quanto pelo clique no
// donut/legenda do resumo (ver renderAchadosResumo) — mesmo efeito de
// clicar o chip correspondente, sem duplicar a lógica de estado+re-render.
function pidDcSelecionarSeveridade(severidade) {
filtroSeveridade = severidade;
document
.querySelectorAll("#dc-achados-severidade-chips .dc-chip")
.forEach((c) => c.classList.toggle("is-active", c.getAttribute("data-dc-severidade") === severidade));
renderAchados();
}
document.querySelectorAll("#dc-achados-severidade-chips [data-dc-severidade]").forEach((chip) => {
chip.addEventListener("click", () => pidDcSelecionarSeveridade(chip.getAttribute("data-dc-severidade")));
});
document.querySelectorAll("#dc-achados-status-chips [data-dc-status]").forEach((chip) => {
chip.addEventListener("click", () => {
filtroStatusAchado = chip.getAttribute("data-dc-status");
document.querySelectorAll("#dc-achados-status-chips .dc-chip").forEach((c) => c.classList.toggle("is-active", c === chip));
renderAchados();
});
});
document.getElementById("dc-regra-filtro-limpar-btn").addEventListener("click", () => {
filtroRegra = null;
renderAchados();
});
// O código da conta (ex. "1.01.01.002.001") já chega do backend na mesma
// ordem hierárquica/sequencial do PDF (uma sintética seguida das suas
// analíticas, ver portal_api/dashboard_contabil/CLAUDE.md) — o nível de
// indentação vem direto da contagem de segmentos separados por ".", sem
// precisar reconstruir uma árvore por id/parent (o mesmo código pode se
// repetir em várias linhas irmãs, ex. três bancos diferentes sob o mesmo
// "1.01.01.002.001", então não dá pra usar o código como chave única).
function dcContaNivel(conta) {
return conta.codigo.split(".").length - 1;
}
// A partir deste nível (equivalente ao 3º segmento do código de
// classificação, ex. "1.01.01") o Balancete/DRE nascem recolhidos —
// pedido explícito do usuário pra reduzir a poluição visual da árvore
// completa; o contador expande sob demanda pra ver o nível analítico
// abaixo. Mesmo limiar usado no relatório "Gerar Dashboard"
// (_CONTABIL_NIVEL_ABERTO_PADRAO em views.py).
const PID_DC_NIVEL_ABERTO_PADRAO = 2;
function dcColapsoPadrao(itens, nivelFn) {
const niveis = itens.map(nivelFn);
const colapsadas = new Set();
itens.forEach((item, i) => {
const temFilhos = i + 1 < itens.length && niveis[i + 1] > niveis[i];
if (temFilhos && niveis[i] >= PID_DC_NIVEL_ABERTO_PADRAO) colapsadas.add(item.id);
});
return colapsadas;
}
// Destaque inicial (antes de qualquer clique) — pedido explícito do
// usuário depois de notar que folhas genuínas (conta/linha sem filho
// nenhum, ex. "(-) SIMPLES NACIONAL") ficavam 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 linhas que TÊM
// filhos escondidos) deixa de fora exatamente essas folhas, já que elas
// nunca entram nesse conjunto (não têm filho pra esconder).
//
// 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
// — ou seja, nada foi revelado logo abaixo dela agora, seja porque ela
// está colapsada (filhos escondidos) ou porque é uma folha sem filho.
function dcUltimaLevaVisivel(itens, nivelFn, colapsadas) {
const niveis = itens.map(nivelFn);
const temFilhos = itens.map((_, i) => i + 1 < itens.length && niveis[i + 1] > niveis[i]);
const visiveis = [];
const pilha = [];
itens.forEach((item, i) => {
const nivel = niveis[i];
while (pilha.length && pilha[pilha.length - 1] >= nivel) pilha.pop();
const oculta = pilha.length > 0;
const temFilhoOculto = temFilhos[i] && colapsadas.has(item.id);
if (temFilhoOculto) pilha.push(nivel);
if (!oculta) visiveis.push({ id: item.id, nivel, temFilhoOculto });
});
const destaque = new Set();
visiveis.forEach((visivel, i) => {
const proximo = visiveis[i + 1];
const eraFimDoRamo = !proximo || proximo.nivel <= visivel.nivel;
if (!eraFimDoRamo) return;
// Nível 0 só entra no destaque quando realmente esconde algo (grupo
// colapsado manualmente) — pedido explícito do usuário: uma sequência
// de linhas de resultado irmãs no nível raiz da DRE (ex. "RESULTADO
// ANTES DA CS E IR", "(=) PREJUÍZO LÍQUIDO DO EXERCÍCIO") não é o fim
// de um ramo aninhado, são linhas soltas lado a lado — não deviam
// herdar o destaque pensado pra folha/grupo aninhado.
if (visivel.nivel === 0 && !visivel.temFilhoOculto) return;
destaque.add(visivel.id);
});
return destaque;
}
// Ids da última "leva" de linhas recém-reveladas por um clique de
// expandir — pedido explícito do usuário: destacar as contas que
// aparecem ao expandir um grupo, e mover o destaque (não acumular) pra a
// leva mais recente conforme o contador vai expandindo mais fundo. Cada
// clique de expandir SUBSTITUI esse conjunto, empilhando o que estava
// destacado antes em dc*DestaqueHistorico; um clique de recolher desempilha
// e restaura o destaque anterior (pedido explícito do usuário — fechar uma
// leva revela de novo o destaque de onde se estava antes de abri-la, em vez
// de simplesmente apagar o destaque). Um par conjunto+pilha por árvore, já
// que Balancete e DRE são independentes.
let dcContasDestaque = new Set();
let dcDreDestaque = new Set();
let dcAvDestaque = new Set();
let dcContasDestaqueHistorico = [];
let dcDreDestaqueHistorico = [];
let dcAvDestaqueHistorico = [];
// Filhos DIRETOS de um item (mesmo próximo nível, não os netos) — usado
// pra saber quais linhas acabaram de ficar visíveis ao expandir `id`.
function dcFilhosDiretos(itens, nivelFn, id) {
const niveis = itens.map(nivelFn);
const idx = itens.findIndex((item) => item.id === id);
const filhos = new Set();
if (idx === -1) return filhos;
const nivelPai = niveis[idx];
for (let j = idx + 1; j < itens.length; j++) {
if (niveis[j] <= nivelPai) break;
if (niveis[j] === nivelPai + 1) filhos.add(itens[j].id);
}
return filhos;
}
// TODOS os descendentes de um item (não só os diretos — filhos, netos,
// bisnetos...) — usado pelo estado tri-state do botão "validado" de uma
// conta/linha sintética (com filhos), ver dcEstadoValidacaoGrupo() logo
// abaixo e o clique em [data-dc-conta-validado]/[data-dc-dre-validado].
function dcDescendentes(itens, nivelFn, id) {
const niveis = itens.map(nivelFn);
const idx = itens.findIndex((item) => item.id === id);
const descendentes = [];
if (idx === -1) return descendentes;
const nivelPai = niveis[idx];
for (let j = idx + 1; j < itens.length; j++) {
if (niveis[j] <= nivelPai) break;
descendentes.push(itens[j]);
}
return descendentes;
}
// Estado do botão "validado" de uma conta/linha COM filhos (sintética) —
// pedido explícito do usuário: "nenhum" (cor padrão) quando nem ela nem
// nenhum descendente está validado; "completo" (verde, mesma cor de uma
// folha validada) quando TODO descendente está validado — independente do
// campo `validado` da própria sintética, já que "está tudo marcado" é o
// que importa; "parcial" (amarelo) em qualquer combinação intermediária —
// inclui o caso de só a própria sintética ter sido marcada (primeiro
// clique), sinalizando "em andamento" até os descendentes acompanharem.
function dcEstadoValidacaoGrupo(item, descendentes) {
if (descendentes.length && descendentes.every((d) => d.validado)) return "completo";
if (item.validado || descendentes.some((d) => d.validado)) return "parcial";
return "nenhum";
}
// Classe/título/aria do botão "validado" de uma linha — folha (`!temFilhosItem`)
// continua o toggle simples de sempre; sintética (com filhos) usa o
// tri-state de dcEstadoValidacaoGrupo (nenhum/parcial/completo).
function dcValidadoInfo(item, itens, nivelFn, temFilhosItem) {
if (!temFilhosItem) {
return {
classe: item.validado ? " dc-conta-validado-btn--validado" : "",
title: item.validado ? "Validada — clique para desmarcar" : "Marcar como validada",
label: item.validado ? "Desmarcar validação" : "Marcar como validada",
pressed: item.validado,
};
}
const estado = dcEstadoValidacaoGrupo(item, dcDescendentes(itens, nivelFn, item.id));
if (estado === "completo") {
return {
classe: " dc-conta-validado-btn--validado",
title: "Grupo totalmente validado — clique para desmarcar tudo",
label: "Desmarcar validação de todo o grupo",
pressed: true,
};
}
if (estado === "parcial") {
return {
classe: " dc-conta-validado-btn--parcial",
title: "Algumas contas do grupo já validadas — clique para validar todas",
label: "Validar todas as contas do grupo",
pressed: false,
};
}
return { classe: "", title: "Marcar como validada", label: "Marcar como validada", pressed: false };
}
// Clique no botão "validado" de uma conta do Balancete — pedido explícito
// do usuário: numa folha (sem filhos) é só um toggle simples; numa
// sintética (com filhos) o clique se comporta em 3 etapas conforme o
// estado atual do grupo (ver dcEstadoValidacaoGrupo):
// "nenhum" → marca só a própria sintética (passa a "parcial").
// "parcial" → pergunta antes de validar TODAS as contas do grupo de
// uma vez (passa a "completo").
// "completo" → desmarca a própria sintética + todas as contas do grupo,
// sem perguntar (mesmo pedido explícito: "apertar
// novamente desmarca todo o grupo").
// Sempre termina com renderContas() — inclusive no caso simples de folha —
// pra recalcular a cor de qualquer sintética ancestral que dependa deste
// item (o estado dela é derivado dos descendentes a cada render, nunca
// armazenado à parte).
async function dcClicarValidadoConta(id) {
const contas = apuracaoAtual.contas;
const idx = contas.findIndex((c) => c.id === id);
if (idx === -1) return;
const niveis = contas.map(dcContaNivel);
const temFilhosItem = idx + 1 < contas.length && niveis[idx + 1] > niveis[idx];
const conta = contas[idx];
if (!temFilhosItem) {
const atualizada = await pidAtualizarValidadoContaContabil(id, !conta.validado);
contas[idx] = atualizada;
renderContas();
return;
}
const descendentes = dcDescendentes(contas, dcContaNivel, id);
const estado = dcEstadoValidacaoGrupo(conta, descendentes);
if (estado === "completo") {
const alvos = [conta, ...descendentes].filter((item) => item.validado);
const respostas = await Promise.all(alvos.map((item) => pidAtualizarValidadoContaContabil(item.id, false)));
respostas.forEach((atualizada) => {
const i = contas.findIndex((c) => c.id === atualizada.id);
if (i >= 0) contas[i] = atualizada;
});
renderContas();
return;
}
if (estado === "parcial") {
if (!(await pidConfirm("Deseja validar todas as contas deste grupo?"))) return;
const alvos = [conta, ...descendentes].filter((item) => !item.validado);
const respostas = await Promise.all(alvos.map((item) => pidAtualizarValidadoContaContabil(item.id, true)));
respostas.forEach((atualizada) => {
const i = contas.findIndex((c) => c.id === atualizada.id);
if (i >= 0) contas[i] = atualizada;
});
renderContas();
return;
}
const atualizada = await pidAtualizarValidadoContaContabil(id, true);
contas[idx] = atualizada;
renderContas();
}
// Mesma lógica de dcClicarValidadoConta(), sobre ContabilLinhaDre.
async function dcClicarValidadoLinha(id) {
const linhas = apuracaoAtual.linhas_dre;
const nivelFn = (l) => Math.max(0, l.nivel);
const idx = linhas.findIndex((l) => l.id === id);
if (idx === -1) return;
const niveis = linhas.map(nivelFn);
const temFilhosItem = idx + 1 < linhas.length && niveis[idx + 1] > niveis[idx];
const linha = linhas[idx];
if (!temFilhosItem) {
const atualizada = await pidAtualizarValidadoLinhaDreContabil(id, !linha.validado);
linhas[idx] = atualizada;
renderDre();
return;
}
const descendentes = dcDescendentes(linhas, nivelFn, id);
const estado = dcEstadoValidacaoGrupo(linha, descendentes);
if (estado === "completo") {
const alvos = [linha, ...descendentes].filter((item) => item.validado);
const respostas = await Promise.all(alvos.map((item) => pidAtualizarValidadoLinhaDreContabil(item.id, false)));
respostas.forEach((atualizada) => {
const i = linhas.findIndex((l) => l.id === atualizada.id);
if (i >= 0) linhas[i] = atualizada;
});
renderDre();
return;
}
if (estado === "parcial") {
if (!(await pidConfirm("Deseja validar todas as contas deste grupo?"))) return;
const alvos = [linha, ...descendentes].filter((item) => !item.validado);
const respostas = await Promise.all(alvos.map((item) => pidAtualizarValidadoLinhaDreContabil(item.id, true)));
respostas.forEach((atualizada) => {
const i = linhas.findIndex((l) => l.id === atualizada.id);
if (i >= 0) linhas[i] = atualizada;
});
renderDre();
return;
}
const atualizada = await pidAtualizarValidadoLinhaDreContabil(id, true);
linhas[idx] = atualizada;
renderDre();
}
// Mesma lógica de dcClicarValidadoConta()/dcClicarValidadoLinha(), sobre
// ContabilLinhaAnaliseVertical.
async function dcClicarValidadoAv(id) {
const linhas = apuracaoAtual.linhas_analise_vertical;
const nivelFn = (l) => Math.max(0, l.nivel);
const idx = linhas.findIndex((l) => l.id === id);
if (idx === -1) return;
const niveis = linhas.map(nivelFn);
const temFilhosItem = idx + 1 < linhas.length && niveis[idx + 1] > niveis[idx];
const linha = linhas[idx];
if (!temFilhosItem) {
const atualizada = await pidAtualizarValidadoLinhaAnaliseVerticalContabil(id, !linha.validado);
linhas[idx] = atualizada;
renderAnaliseVertical();
return;
}
const descendentes = dcDescendentes(linhas, nivelFn, id);
const estado = dcEstadoValidacaoGrupo(linha, descendentes);
if (estado === "completo") {
const alvos = [linha, ...descendentes].filter((item) => item.validado);
const respostas = await Promise.all(
alvos.map((item) => pidAtualizarValidadoLinhaAnaliseVerticalContabil(item.id, false))
);
respostas.forEach((atualizada) => {
const i = linhas.findIndex((l) => l.id === atualizada.id);
if (i >= 0) linhas[i] = atualizada;
});
renderAnaliseVertical();
return;
}
if (estado === "parcial") {
if (!(await pidConfirm("Deseja validar todas as contas deste grupo?"))) return;
const alvos = [linha, ...descendentes].filter((item) => !item.validado);
const respostas = await Promise.all(
alvos.map((item) => pidAtualizarValidadoLinhaAnaliseVerticalContabil(item.id, true))
);
respostas.forEach((atualizada) => {
const i = linhas.findIndex((l) => l.id === atualizada.id);
if (i >= 0) linhas[i] = atualizada;
});
renderAnaliseVertical();
return;
}
const atualizada = await pidAtualizarValidadoLinhaAnaliseVerticalContabil(id, true);
linhas[idx] = atualizada;
renderAnaliseVertical();
}
// Conteúdo do botão de observação de uma conta/linha — pedido explícito
// do usuário: a coluna "Observação" ficava poluída visualmente com
// "+ Observação"/o texto da observação em si repetido em toda linha;
// virou sempre o mesmo ícone (o `title`/tooltip mostra o texto da
// observação quando houver, "Adicionar observação" quando não) — só a cor
// muda (`.dc-conta-observacao-btn--preenchida`, ver dashboard-contabil.css)
// pra indicar de relance quais linhas já têm observação, sem precisar
// mostrar o texto inteiro na tabela. Clicar não abre mais um popup — abre
// um editor inline dentro da própria tabela (ver dcContaObsEditId/
// dcDreObsEditId acima e os `
` abaixo), pedido
// explícito do usuário pra não perder o texto por causa de um clique fora
// de um modal.
const PID_DC_OBSERVACAO_ICONE =
'';
// Botão de "validado" (check) ao lado do de observação — pedido explícito
// do usuário, um segundo controle puramente informativo pro contador
// marcar que já conferiu aquela conta/linha durante a revisão (não afeta
// achados/observação/relatório, só o próprio marcador).
const PID_DC_VALIDADO_ICONE =
'';
function renderContas() {
const body = document.getElementById("dc-contas-body");
body.innerHTML = "";
const concluida = apuracaoAtual.status === "concluida";
const contas = apuracaoAtual.contas;
const niveis = contas.map(dcContaNivel);
const temFilhos = contas.map((_, i) => i + 1 < contas.length && niveis[i + 1] > niveis[i]);
// Pilha dos níveis de ancestrais recolhidos — enquanto uma linha tiver
// algum ancestral na pilha, ela fica oculta (é descendente de um grupo
// fechado), mesmo padrão de renderização de árvore a partir de uma
// lista plana com profundidade já conhecida.
const pilhaColapso = [];
contas.forEach((conta, i) => {
const nivel = niveis[i];
while (pilhaColapso.length && pilhaColapso[pilhaColapso.length - 1] >= nivel) {
pilhaColapso.pop();
}
const oculta = pilhaColapso.length > 0;
if (temFilhos[i] && dcContasColapsadas.has(conta.id)) {
pilhaColapso.push(nivel);
}
if (oculta) return;
const expandida = !dcContasColapsadas.has(conta.id);
const toggle = temFilhos[i]
? ``
: ``;
const validadoInfo = dcValidadoInfo(conta, contas, dcContaNivel, temFilhos[i]);
const tr = document.createElement("tr");
if (conta.tipo === "S") tr.classList.add("dc-conta-row--sintetica");
if (dcContasDestaque.has(conta.id)) tr.classList.add("dc-conta-row--destaque");
tr.innerHTML = `
`;
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" ----
// Opções de ícone do card no relatório "Gerar Dashboard" — cópia irmã de
// `_CONTABIL_ICONES_SVG` em portal_api/views.py (mesmas chaves/desenhos),
// mantida em sincronia manualmente (ver comentário em
// IndicadorContabilDefinicao.icone, models.py, sobre por que não dá pra
// ter uma fonte única entre o backend e este JS).
const PID_DC_INDICADOR_ICONES = [
{ chave: "barras", rotulo: "Barras", svg: '' },
{
chave: "tendencia-alta",
rotulo: "Tendência de alta",
svg: '',
},
{
chave: "tendencia-baixa",
rotulo: "Tendência de baixa",
svg: '',
},
{
chave: "percentual",
rotulo: "Percentual",
svg: '',
},
{
chave: "pizza",
rotulo: "Gráfico de pizza",
svg: '',
},
{ chave: "atividade", rotulo: "Atividade", svg: '' },
{
chave: "moeda",
rotulo: "Cifrão",
svg: '',
},
{
chave: "alvo",
rotulo: "Alvo/meta",
svg: '',
},
{
chave: "camadas",
rotulo: "Camadas",
svg:
'' +
'',
},
{
chave: "cartao",
rotulo: "Cartão",
svg: '',
},
{
chave: "selo",
rotulo: "Selo/prêmio",
svg: '',
},
];
const PID_DC_INDICADOR_ICONE_PADRAO = "barras";
const indicadorModal = document.getElementById("dc-indicador-modal");
const indicadorComponentesEl = document.getElementById("dc-indicador-componentes");
const indicadorIconesEl = document.getElementById("dc-indicador-icones");
let dcIndicadorEmEdicao = null; // definição completa (id/chave/.../componentes) ou null = criando
let dcIndicadorIconeSelecionado = PID_DC_INDICADOR_ICONE_PADRAO;
let dcIndicadorComponenteSeq = 0;
// Grade de ícones é montada uma única vez (não depende da definição em
// edição, só a seleção ativa muda) — cada botão já é o próprio preview do
// ícone, sem precisar de nenhum outro elemento de preview separado.
PID_DC_INDICADOR_ICONES.forEach((icone) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "dc-ind-icone-btn";
btn.setAttribute("data-dc-icone", icone.chave);
btn.setAttribute("aria-label", icone.rotulo);
btn.title = icone.rotulo;
btn.innerHTML = ``;
indicadorIconesEl.appendChild(btn);
});
function pidDcSelecionaIcone(chave) {
dcIndicadorIconeSelecionado = chave;
indicadorIconesEl.querySelectorAll(".dc-ind-icone-btn").forEach((btn) => {
btn.classList.toggle("is-selecionado", btn.getAttribute("data-dc-icone") === chave);
});
}
indicadorIconesEl.addEventListener("click", (event) => {
const btn = event.target.closest(".dc-ind-icone-btn");
if (btn) pidDcSelecionaIcone(btn.getAttribute("data-dc-icone"));
});
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 = `
`;
} 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 = `
`;
} 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.
`;
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-formula-exibicao").value = definicao ? definicao.formula_exibicao : "";
document.getElementById("dc-indicador-modal-padrao").checked = definicao ? definicao.padrao : true;
pidDcSelecionaIcone(definicao ? definicao.icone : PID_DC_INDICADOR_ICONE_PADRAO);
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;
}
// Fechar por "Cancelar" ou clicando fora (overlay) sempre pede confirmação
// — pedido explícito do usuário, mesmo padrão já usado no modal de "Mais
// informações" (ajuda-aplicacao.js) e no antigo "Fechar sem salvar" do
// Cadastro de Regras de Plano de Saúde (ver "Modal de confirmação
// genérico" no CLAUDE.md da raiz). Só esses dois caminhos passam por
// aqui — depois de salvar/excluir com sucesso, o próprio fluxo chama
// `pidDcFecharIndicadorModal()` direto, sem perguntar (não há o que
// descartar).
async function pidDcFecharIndicadorModalComConfirmacao() {
if (!(await pidConfirm("Sair sem salvar as alterações?", { perigoso: true }))) return;
pidDcFecharIndicadorModal();
}
document.getElementById("dc-indicador-modal-cancel-btn").addEventListener("click", pidDcFecharIndicadorModalComConfirmacao);
indicadorModal.addEventListener("click", (event) => {
if (event.target === indicadorModal) pidDcFecharIndicadorModalComConfirmacao();
});
// 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(),
formula_exibicao: document.getElementById("dc-indicador-modal-formula-exibicao").value.trim(),
padrao: document.getElementById("dc-indicador-modal-padrao").checked,
icone: dcIndicadorIconeSelecionado,
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.