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 =
'';
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");
}
async function pidAtualizarObservacaoContaContabil(id, observacao) {
return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: { observacao } });
}
async function pidAtualizarObservacaoLinhaDreContabil(id, observacao) {
return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: { observacao } });
}
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 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;
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();
// 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;
}
function renderContas() {
const body = document.getElementById("dc-contas-body");
body.innerHTML = "";
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 tr = document.createElement("tr");
if (conta.tipo === "S") tr.classList.add("dc-conta-row--sintetica");
tr.innerHTML = `
${pidDcEscapeHtml(conta.codigo)}
${toggle}${pidDcEscapeHtml(conta.descricao)}
${pidDcFormatMoeda(conta.saldo_anterior)}
${pidDcFormatMoeda(conta.debito)}
${pidDcFormatMoeda(conta.credito)}
${pidDcFormatMoeda(conta.saldo_atual)}
`;
body.appendChild(tr);
});
}
document.getElementById("dc-contas-body").addEventListener("click", (event) => {
const toggleBtn = event.target.closest("[data-dc-conta-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-conta-toggle"));
if (dcContasColapsadas.has(id)) dcContasColapsadas.delete(id);
else dcContasColapsadas.add(id);
renderContas();
return;
}
const btn = event.target.closest("[data-dc-conta-observacao]");
if (!btn) return;
abrirModalObservacao("conta", Number(btn.getAttribute("data-dc-conta-observacao")));
});
// Mesma árvore recolhível do Balancete (ver renderContas), agora sobre
// ContabilLinhaDre.nivel — que já chega pronto do backend na ordem de
// leitura do PDF, então "ter filhos" é só olhar se a próxima linha tem
// nível maior que o da linha atual.
function renderDre() {
const body = document.getElementById("dc-dre-body");
body.innerHTML = "";
const linhas = apuracaoAtual.linhas_dre;
const niveis = linhas.map((linha) => Math.max(0, linha.nivel));
const temFilhos = linhas.map((_, i) => i + 1 < linhas.length && niveis[i + 1] > niveis[i]);
const pilhaColapso = [];
linhas.forEach((linha, i) => {
const nivel = niveis[i];
while (pilhaColapso.length && pilhaColapso[pilhaColapso.length - 1] >= nivel) {
pilhaColapso.pop();
}
const oculta = pilhaColapso.length > 0;
if (temFilhos[i] && dcDreColapsadas.has(linha.id)) {
pilhaColapso.push(nivel);
}
if (oculta) return;
const expandida = !dcDreColapsadas.has(linha.id);
const toggle = temFilhos[i]
? ``
: ``;
const tr = document.createElement("tr");
tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row";
const indentacao = nivel * 16;
tr.innerHTML = `
${toggle}${pidDcEscapeHtml(linha.descricao)}
${pidDcFormatMoeda(linha.valor)}
`;
body.appendChild(tr);
});
}
document.getElementById("dc-dre-body").addEventListener("click", (event) => {
const toggleBtn = event.target.closest("[data-dc-dre-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-dre-toggle"));
if (dcDreColapsadas.has(id)) dcDreColapsadas.delete(id);
else dcDreColapsadas.add(id);
renderDre();
return;
}
const btn = event.target.closest("[data-dc-dre-observacao]");
if (!btn) return;
abrirModalObservacao("dre", Number(btn.getAttribute("data-dc-dre-observacao")));
});
// ---- Tabs ----
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");
document.querySelectorAll("#dc-tabs .pa-tab").forEach((t) => t.classList.toggle("is-active", t === tabBtn));
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 = `
`;
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 = `
`;
} 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.