portal_publico/static/js/dashboard-contabil.js

2724 lines
120 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 =
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7Z"/><circle cx="12" cy="12" r="3"/></svg>';
const PID_DC_ICON_OLHO_FECHADO =
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 7 11 7a13.16 13.16 0 0 1-1.67 2.68M6.61 6.61C3.35 8.44 1 12 1 12s4 7 11 7a9.26 9.26 0 0 0 5.39-1.61M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
const PID_DC_ICON_EDITAR =
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>';
const PID_DC_ICON_LIXEIRA =
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" stroke-linecap="round" stroke-linejoin="round"/></svg>';
// 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 = `
<td>${pidDcEscapeHtml(apuracao.codigo_empresa)} - ${pidDcEscapeHtml(apuracao.nome_empresa)}</td>
<td>${pidDcFormatCompetencia(apuracao.competencia)}</td>
<td>${apuracao.total_achados_pendentes}</td>
<td><span class="status-pill ${apuracao.status === "concluida" ? "status-pill--ativo" : "status-pill--inativo"}">${PID_DC_STATUS_LABELS[apuracao.status] || apuracao.status}</span></td>
<td>${pidDcEscapeHtml(apuracao.criado_por_nome || "-")}</td>
<td>${pidDcFormatData(apuracao.criado_em)}</td>
<td class="pa-row-actions">
<button type="button" class="btn-outline" data-dc-abrir="${apuracao.id}">Abrir</button>
<button type="button" class="icon-btn" data-dc-excluir="${apuracao.id}" aria-label="Excluir" title="Excluir">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</td>
`;
body.appendChild(tr);
});
}
document.getElementById("dc-list-body").addEventListener("click", async (event) => {
const abrirBtn = event.target.closest("[data-dc-abrir]");
if (abrirBtn) {
await abrirRevisao(Number(abrirBtn.getAttribute("data-dc-abrir")));
return;
}
const excluirBtn = event.target.closest("[data-dc-excluir]");
if (excluirBtn) {
if (!(await pidConfirm("Excluir esta análise? Essa ação não pode ser desfeita.", { perigoso: true }))) return;
await pidExcluirApuracaoContabil(Number(excluirBtn.getAttribute("data-dc-excluir")));
await carregarLista();
}
});
// ---- Nova Análise ----
function wireArquivoField(inputId) {
const input = document.getElementById(inputId);
const nameEl = document.getElementById(`${inputId}-name`);
const removeBtn = document.getElementById(`${inputId}-remove`);
function atualizar() {
const arquivo = input.files[0];
nameEl.textContent = arquivo ? arquivo.name : "Nenhum arquivo selecionado";
removeBtn.hidden = !arquivo;
}
input.addEventListener("change", atualizar);
removeBtn.addEventListener("click", () => {
input.value = "";
atualizar();
});
return atualizar;
}
const resetArquivoField = wireArquivoField("dc-form-arquivo");
document.getElementById("dc-new-btn").addEventListener("click", () => {
document.getElementById("dc-form-arquivo").value = "";
resetArquivoField();
document.getElementById("dc-form-error").textContent = "";
showView("form");
});
document.getElementById("dc-form-back-btn").addEventListener("click", () => showView("list"));
document.getElementById("dc-form-cancel-btn").addEventListener("click", () => showView("list"));
document.getElementById("dc-form-submit-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("dc-form-error");
errorEl.textContent = "";
const arquivo = document.getElementById("dc-form-arquivo").files[0];
if (!arquivo) {
errorEl.textContent = "Anexe o PDF de Balancete + DRE.";
return;
}
const formData = new FormData();
formData.append("arquivo", arquivo);
const submitBtn = document.getElementById("dc-form-submit-btn");
submitBtn.disabled = true;
try {
const apuracao = await pidCriarApuracaoContabil(formData);
apuracaoAtual = apuracao;
filtroSeveridade = "todas";
filtroStatusAchado = "pendente";
filtroRegra = null;
renderRevisao();
showView("review");
await carregarLista();
} catch (e) {
errorEl.textContent = e.message;
} finally {
submitBtn.disabled = false;
}
});
// ---- Revisão ----
async function abrirRevisao(id) {
apuracaoAtual = await pidFetchApuracaoContabil(id);
filtroSeveridade = "todas";
filtroStatusAchado = "pendente";
filtroRegra = null;
renderRevisao();
showView("review");
}
function renderRevisao() {
const apuracao = apuracaoAtual;
const concluida = apuracao.status === "concluida";
document.getElementById("dc-review-empresa").textContent = `— ${apuracao.codigo_empresa} - ${apuracao.nome_empresa}`;
document.getElementById("dc-review-subtitle").textContent =
`CNPJ ${apuracao.cnpj || "não informado"} · Competência ${pidDcFormatCompetencia(apuracao.competencia)} · ${apuracao.contas.length} conta(s)`;
document.getElementById("dc-concluida-aviso").hidden = !concluida;
document.getElementById("dc-concluir-btn").hidden = concluida;
dcContasColapsadas = dcColapsoPadrao(apuracao.contas, dcContaNivel);
dcDreColapsadas = dcColapsoPadrao(apuracao.linhas_dre, (linha) => Math.max(0, linha.nivel));
// Destaque inicial = a última leva já visível de cada ramo (grupo
// colapsado por padrão OU folha genuína sem filho nenhum — ver
// dcUltimaLevaVisivel()) — nasce destacada sem precisar de nenhum
// clique, mesmo ajuste já feito no relatório "Gerar Dashboard" (ver
// pidDcrArvore() em dashboard-contabil-relatorio.html), pedido explícito
// do usuário pra não abrir a tela inteira na cor de grupo/total.
dcContasDestaque = dcUltimaLevaVisivel(apuracao.contas, dcContaNivel, dcContasColapsadas);
dcDreDestaque = dcUltimaLevaVisivel(apuracao.linhas_dre, (linha) => Math.max(0, linha.nivel), dcDreColapsadas);
dcContasDestaqueHistorico = [];
dcDreDestaqueHistorico = [];
dcContaObsEditId = null;
dcDreObsEditId = null;
dcAchadosContaExpandida = new Set();
dcIndicadoresAtual = null;
// Aba "Análise Vertical" só existe quando o PDF tinha essa seção (nem
// todo relatório traz — ver ContabilApuracao.analise_vertical_meses) —
// some completamente quando vazia, em vez de mostrar uma tabela sem
// nenhuma coluna de mês.
const temAnaliseVertical = (apuracao.analise_vertical_meses || []).length > 0;
const avTabBtn = document.getElementById("dc-av-tab-btn");
const avEstavaAtiva = avTabBtn.classList.contains("is-active");
avTabBtn.hidden = !temAnaliseVertical;
if (!temAnaliseVertical && avEstavaAtiva) {
// A apuração anterior tinha Análise Vertical e estava nessa aba — como
// esta não tem, volta pra "Observações" pra não deixar o botão da aba
// ativa escondido com o painel dela ainda visível.
document.querySelectorAll("#dc-tabs .pa-tab").forEach((t) => t.classList.toggle("is-active", t.dataset.dcTab === "achados"));
document.querySelectorAll("[data-dc-panel]").forEach((panel) => {
panel.hidden = panel.getAttribute("data-dc-panel") !== "achados";
});
}
dcAvColapsadas = dcColapsoPadrao(apuracao.linhas_analise_vertical, (linha) => Math.max(0, linha.nivel));
dcAvDestaque = dcUltimaLevaVisivel(
apuracao.linhas_analise_vertical,
(linha) => Math.max(0, linha.nivel),
dcAvColapsadas
);
dcAvDestaqueHistorico = [];
dcAvObsEditId = null;
renderAchados();
renderContas();
renderDre();
renderAnaliseVertical();
renderResumoFechamento();
}
const PID_DC_SEVERIDADE_ORDEM = { alta: 0, media: 1, baixa: 2 };
function achadosFiltrados() {
return apuracaoAtual.achados
.filter((achado) => {
if (filtroSeveridade !== "todas" && achado.severidade !== filtroSeveridade) return false;
if (filtroStatusAchado === "pendente" && achado.status !== "pendente") return false;
if (filtroRegra && achado.regra !== filtroRegra) return false;
return true;
})
.slice()
.sort((a, b) => (PID_DC_SEVERIDADE_ORDEM[a.severidade] ?? 3) - (PID_DC_SEVERIDADE_ORDEM[b.severidade] ?? 3));
}
function pidDcScrollParaLista() {
document.getElementById("dc-achados-list").scrollIntoView({ behavior: "smooth", block: "nearest" });
}
// Resumo do topo da aba Observações: donut por severidade (clicável, filtra
// igual aos chips Alta/Média/Baixa) + grade de cards por categoria/regra
// (clicáveis, isolam achado.regra na lista abaixo). Contagens sempre sobre
// TODOS os achados da apuração, independente dos filtros já aplicados —
// visão geral estável, mesmo espírito do "Distribuição das Análises" do
// sistema externo que inspirou esta seção.
function renderAchadosResumo() {
const achados = apuracaoAtual.achados;
const total = achados.length;
// ---- Donut por severidade ----
const porSeveridade = { alta: 0, media: 0, baixa: 0 };
achados.forEach((achado) => {
if (achado.severidade in porSeveridade) porSeveridade[achado.severidade] += 1;
});
document.getElementById("dc-achados-donut-total").textContent = String(total);
const svg = document.getElementById("dc-achados-donut-svg");
const legend = document.getElementById("dc-achados-donut-legend");
legend.innerHTML = "";
let anelSvg = `<circle class="dc-donut-svg__bg" cx="18" cy="18" r="15.9155" />`;
if (total > 0) {
let acumulado = 0;
PID_DC_SEVERIDADE_LISTA.forEach((severidade) => {
const qtd = porSeveridade[severidade];
if (qtd === 0) return;
const pct = (qtd / total) * 100;
const offset = 25 - acumulado;
acumulado += pct;
anelSvg += `
<circle
class="dc-donut-svg__seg${filtroSeveridade === severidade ? " is-active" : ""}"
data-dc-donut-severidade="${severidade}"
cx="18" cy="18" r="15.9155"
stroke="${PID_DC_SEVERIDADE_CORES[severidade]}"
stroke-dasharray="${pct} ${100 - pct}"
stroke-dashoffset="${offset}"
/>
`;
});
}
svg.innerHTML = anelSvg;
svg.querySelectorAll("[data-dc-donut-severidade]").forEach((seg) => {
seg.addEventListener("click", () => {
pidDcSelecionarSeveridade(seg.getAttribute("data-dc-donut-severidade"));
pidDcScrollParaLista();
});
});
PID_DC_SEVERIDADE_LISTA.forEach((severidade) => {
const li = document.createElement("li");
li.className = `dc-donut-legend__item${filtroSeveridade === severidade ? " is-active" : ""}`;
li.innerHTML = `<span class="dc-donut-legend__dot" style="background:${PID_DC_SEVERIDADE_CORES[severidade]}"></span>${PID_DC_SEVERIDADE_LABELS[severidade]} (${porSeveridade[severidade]})`;
li.addEventListener("click", () => {
pidDcSelecionarSeveridade(severidade);
pidDcScrollParaLista();
});
legend.appendChild(li);
});
// ---- Cards por grupo temático (ver PID_DC_GRUPOS) ----
const porRegra = new Map();
PID_DC_REGRAS.forEach((r) => porRegra.set(r.chave, []));
achados.forEach((achado) => {
if (!porRegra.has(achado.regra)) porRegra.set(achado.regra, []);
porRegra.get(achado.regra).push(achado);
});
const regraPorChave = new Map(PID_DC_REGRAS.map((r) => [r.chave, r]));
const grid = document.getElementById("dc-achados-categorias");
grid.innerHTML = "";
PID_DC_GRUPOS.forEach((grupo, grupoIndex) => {
const cor = PID_DC_CATEGORIA_CORES[grupoIndex % PID_DC_CATEGORIA_CORES.length];
const totalGrupo = grupo.regras.reduce((soma, chave) => soma + (porRegra.get(chave) || []).length, 0);
const card = document.createElement("div");
card.className = "dc-categoria-card";
card.style.setProperty("--dc-categoria-cor", cor);
card.innerHTML = `
<div class="dc-categoria-card__header">
<span class="dc-categoria-card__dot"></span>
<span class="dc-categoria-card__titulo">${pidDcEscapeHtml(grupo.label)}</span>
<span class="dc-categoria-card__contagem">${totalGrupo}</span>
</div>
<div class="dc-categoria-card__regras"></div>
`;
const regrasEl = card.querySelector(".dc-categoria-card__regras");
grupo.regras.forEach((chave) => {
const regraInfo = regraPorChave.get(chave);
if (!regraInfo) return;
const itens = porRegra.get(chave) || [];
const clicavel = itens.length > 0;
const linha = document.createElement("div");
linha.className = [
"dc-categoria-card__regra",
clicavel ? "dc-categoria-card__regra--clicavel" : "dc-categoria-card__regra--vazia",
filtroRegra === chave ? "is-active" : "",
].filter(Boolean).join(" ");
linha.innerHTML = `
<span class="dc-categoria-card__regra-label">${pidDcEscapeHtml(regraInfo.label)}</span>
<span class="dc-categoria-card__regra-contagem">${itens.length}</span>
`;
if (clicavel) {
linha.addEventListener("click", () => {
filtroRegra = filtroRegra === chave ? null : chave;
renderAchados();
pidDcScrollParaLista();
});
}
regrasEl.appendChild(linha);
});
grid.appendChild(card);
});
}
function renderAchados() {
renderAchadosResumo();
const filtroAtivo = document.getElementById("dc-regra-filtro-ativo");
const regraInfo = filtroRegra ? PID_DC_REGRAS.find((r) => r.chave === filtroRegra) : null;
filtroAtivo.hidden = !regraInfo;
if (regraInfo) document.getElementById("dc-regra-filtro-ativo-label").textContent = regraInfo.label;
const lista = document.getElementById("dc-achados-list");
const empty = document.getElementById("dc-achados-empty");
const achados = achadosFiltrados();
lista.innerHTML = "";
empty.hidden = achados.length > 0;
achados.forEach((achado) => {
const card = document.createElement("div");
card.className = `dc-achado-card dc-achado-card--${achado.severidade}`;
const podeEditar = apuracaoAtual.status !== "concluida" && achado.status === "pendente";
const conta = achado.conta != null ? apuracaoAtual.contas.find((c) => c.id === achado.conta) : null;
const expandida = dcAchadosContaExpandida.has(achado.id);
card.innerHTML = `
<div class="dc-achado-card__main">
<div class="dc-achado-card__titulo">
<span class="dc-badge dc-badge--${achado.severidade}">${PID_DC_SEVERIDADE_LABELS[achado.severidade] || achado.severidade}</span>
<span class="dc-badge dc-badge--${achado.status}">${PID_DC_ACHADO_STATUS_LABELS[achado.status] || achado.status}</span>
${pidDcEscapeHtml(achado.titulo)}
</div>
<div class="dc-achado-card__mensagem">${pidDcEscapeHtml(achado.mensagem)}</div>
${conta ? `
<button type="button" class="dc-achado-card__toggle-conta${expandida ? " is-expanded" : ""}" data-dc-achado-toggle-conta="${achado.id}">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
${expandida ? "Ocultar conta usada no apontamento" : "Ver conta usada no apontamento"}
</button>
${expandida ? `
<dl class="dc-achado-card__conta">
<div><dt>Conta</dt><dd>${pidDcEscapeHtml(conta.codigo)} — ${pidDcEscapeHtml(conta.descricao)}</dd></div>
<div><dt>Saldo Anterior</dt><dd>${pidDcFormatMoeda(conta.saldo_anterior)}</dd></div>
<div><dt>Débito</dt><dd>${pidDcFormatMoeda(conta.debito)}</dd></div>
<div><dt>Crédito</dt><dd>${pidDcFormatMoeda(conta.credito)}</dd></div>
<div><dt>Saldo Atual</dt><dd>${pidDcFormatMoeda(conta.saldo_atual)}</dd></div>
</dl>
` : ""}
` : ""}
${achado.observacao_contador ? `<div class="dc-achado-card__observacao">${pidDcEscapeHtml(achado.observacao_contador)}${achado.tratado_por_nome ? ` — ${pidDcEscapeHtml(achado.tratado_por_nome)}` : ""}</div>` : ""}
</div>
<div class="dc-achado-card__actions">
${podeEditar ? `<button type="button" class="btn-outline" data-dc-achado-editar="${achado.id}">Revisar</button>` : ""}
</div>
`;
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 `<tr class="dc-obs-edit-row">` 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 =
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
// 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 =
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>';
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]
? `<button type="button" class="dc-conta-toggle${expandida ? " is-expanded" : ""}" data-dc-conta-toggle="${conta.id}" aria-label="${expandida ? "Recolher" : "Expandir"} ${pidDcEscapeHtml(conta.descricao)}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>`
: `<span class="dc-conta-toggle-spacer"></span>`;
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 = `
<td>${pidDcEscapeHtml(conta.codigo)}</td>
<td style="padding-left: calc(var(--space-4) + ${nivel * 18}px)">
<span class="dc-conta-desc-cell">${toggle}<span>${pidDcEscapeHtml(conta.descricao)}</span></span>
</td>
<td>${pidDcFormatMoeda(conta.saldo_anterior)}</td>
<td>${pidDcFormatMoeda(conta.debito)}</td>
<td>${pidDcFormatMoeda(conta.credito)}</td>
<td>${pidDcFormatMoeda(conta.saldo_atual)}</td>
<td>
<div class="dc-obs-cell-actions">
<button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-conta-validado="${conta.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}>
${PID_DC_VALIDADO_ICONE}
</button>
<button type="button" class="dc-conta-observacao-btn${conta.observacao ? " dc-conta-observacao-btn--preenchida" : ""}" data-dc-conta-observacao="${conta.id}" title="${pidDcEscapeHtml(conta.observacao || "Adicionar observação")}" aria-label="${conta.observacao ? "Editar observação" : "Adicionar observação"}">
${PID_DC_OBSERVACAO_ICONE}
</button>
</div>
</td>
`;
body.appendChild(tr);
if (dcContaObsEditId === conta.id) {
const editRow = document.createElement("tr");
editRow.className = "dc-obs-edit-row";
editRow.innerHTML = `
<td colspan="7">
<div class="dc-obs-edit">
<div class="modal-field">
<textarea id="dc-conta-obs-texto-${conta.id}" rows="3" placeholder="Anote sua análise..." ${concluida ? "disabled" : ""}>${pidDcEscapeHtml(conta.observacao || "")}</textarea>
</div>
<label class="modal-checkbox">
<input type="checkbox" id="dc-conta-obs-mostrar-${conta.id}" ${conta.observacao && !conta.oculta_no_relatorio ? "checked" : ""} ${concluida ? "disabled" : ""} />
Mostrar esta observação ao cliente no relatório
</label>
<p class="modal-error" id="dc-conta-obs-error-${conta.id}"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" data-dc-conta-obs-cancel="${conta.id}">${concluida ? "Fechar" : "Cancelar"}</button>
${concluida ? "" : `<button type="button" class="btn-solid" data-dc-conta-obs-save="${conta.id}">Salvar</button>`}
</div>
</div>
</td>
`;
body.appendChild(editRow);
}
});
renderContasObsResumo();
}
document.getElementById("dc-contas-body").addEventListener("click", async (event) => {
const toggleBtn = event.target.closest("[data-dc-conta-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-conta-toggle"));
const estavaColapsada = dcContasColapsadas.has(id);
if (estavaColapsada) dcContasColapsadas.delete(id);
else dcContasColapsadas.add(id);
// Expandir empilha o destaque atual e troca pros filhos recém-revelados;
// recolher desempilha e restaura o destaque de antes — ver dcContasDestaque acima.
if (estavaColapsada) {
dcContasDestaqueHistorico.push(dcContasDestaque);
dcContasDestaque = dcFilhosDiretos(apuracaoAtual.contas, dcContaNivel, id);
} else {
dcContasDestaque = dcContasDestaqueHistorico.pop() || new Set();
}
renderContas();
return;
}
const validadoBtn = event.target.closest("[data-dc-conta-validado]");
if (validadoBtn) {
if (validadoBtn.disabled) return;
const id = Number(validadoBtn.getAttribute("data-dc-conta-validado"));
validadoBtn.disabled = true;
try {
await dcClicarValidadoConta(id);
} catch (e) {
await pidAlert(e.message);
} finally {
validadoBtn.disabled = false;
}
return;
}
const obsBtn = event.target.closest("[data-dc-conta-observacao]");
if (obsBtn) {
const id = Number(obsBtn.getAttribute("data-dc-conta-observacao"));
dcContaObsEditId = dcContaObsEditId === id ? null : id;
renderContas();
return;
}
const cancelBtn = event.target.closest("[data-dc-conta-obs-cancel]");
if (cancelBtn) {
dcContaObsEditId = null;
renderContas();
return;
}
const saveBtn = event.target.closest("[data-dc-conta-obs-save]");
if (saveBtn) {
const id = Number(saveBtn.getAttribute("data-dc-conta-obs-save"));
const errorEl = document.getElementById(`dc-conta-obs-error-${id}`);
errorEl.textContent = "";
const texto = document.getElementById(`dc-conta-obs-texto-${id}`).value;
const mostrar = document.getElementById(`dc-conta-obs-mostrar-${id}`).checked;
saveBtn.disabled = true;
try {
const atualizada = await pidAtualizarObservacaoContaContabil(id, {
observacao: texto,
oculta_no_relatorio: !mostrar,
});
const indice = apuracaoAtual.contas.findIndex((c) => c.id === id);
apuracaoAtual.contas[indice] = atualizada;
dcContaObsEditId = null;
renderContas();
} catch (e) {
errorEl.textContent = e.message;
saveBtn.disabled = false;
}
}
});
// 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 concluida = apuracaoAtual.status === "concluida";
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]
? `<button type="button" class="dc-conta-toggle${expandida ? " is-expanded" : ""}" data-dc-dre-toggle="${linha.id}" aria-label="${expandida ? "Recolher" : "Expandir"} ${pidDcEscapeHtml(linha.descricao)}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>`
: `<span class="dc-conta-toggle-spacer"></span>`;
const validadoInfo = dcValidadoInfo(linha, linhas, (l) => Math.max(0, l.nivel), temFilhos[i]);
const tr = document.createElement("tr");
tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row";
if (dcDreDestaque.has(linha.id)) tr.classList.add("dc-conta-row--destaque");
const indentacao = nivel * 16;
tr.innerHTML = `
<td style="padding-left: calc(var(--space-4) + ${indentacao}px)">
<span class="dc-conta-desc-cell">${toggle}<span>${pidDcEscapeHtml(linha.descricao)}</span></span>
</td>
<td>${pidDcFormatMoeda(linha.valor)}</td>
<td>
<div class="dc-obs-cell-actions">
<button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-dre-validado="${linha.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}>
${PID_DC_VALIDADO_ICONE}
</button>
<button type="button" class="dc-conta-observacao-btn${linha.observacao ? " dc-conta-observacao-btn--preenchida" : ""}" data-dc-dre-observacao="${linha.id}" title="${pidDcEscapeHtml(linha.observacao || "Adicionar observação")}" aria-label="${linha.observacao ? "Editar observação" : "Adicionar observação"}">
${PID_DC_OBSERVACAO_ICONE}
</button>
</div>
</td>
`;
body.appendChild(tr);
if (dcDreObsEditId === linha.id) {
const editRow = document.createElement("tr");
editRow.className = "dc-obs-edit-row";
editRow.innerHTML = `
<td colspan="3">
<div class="dc-obs-edit">
<div class="modal-field">
<textarea id="dc-dre-obs-texto-${linha.id}" rows="3" placeholder="Anote sua análise..." ${concluida ? "disabled" : ""}>${pidDcEscapeHtml(linha.observacao || "")}</textarea>
</div>
<label class="modal-checkbox">
<input type="checkbox" id="dc-dre-obs-mostrar-${linha.id}" ${linha.observacao && !linha.oculta_no_relatorio ? "checked" : ""} ${concluida ? "disabled" : ""} />
Mostrar esta observação ao cliente no relatório
</label>
<p class="modal-error" id="dc-dre-obs-error-${linha.id}"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" data-dc-dre-obs-cancel="${linha.id}">${concluida ? "Fechar" : "Cancelar"}</button>
${concluida ? "" : `<button type="button" class="btn-solid" data-dc-dre-obs-save="${linha.id}">Salvar</button>`}
</div>
</div>
</td>
`;
body.appendChild(editRow);
}
});
renderDreObsResumo();
}
document.getElementById("dc-dre-body").addEventListener("click", async (event) => {
const toggleBtn = event.target.closest("[data-dc-dre-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-dre-toggle"));
const estavaColapsada = dcDreColapsadas.has(id);
if (estavaColapsada) dcDreColapsadas.delete(id);
else dcDreColapsadas.add(id);
const nivelFn = (linha) => Math.max(0, linha.nivel);
if (estavaColapsada) {
dcDreDestaqueHistorico.push(dcDreDestaque);
dcDreDestaque = dcFilhosDiretos(apuracaoAtual.linhas_dre, nivelFn, id);
} else {
dcDreDestaque = dcDreDestaqueHistorico.pop() || new Set();
}
renderDre();
return;
}
const validadoBtn = event.target.closest("[data-dc-dre-validado]");
if (validadoBtn) {
if (validadoBtn.disabled) return;
const id = Number(validadoBtn.getAttribute("data-dc-dre-validado"));
validadoBtn.disabled = true;
try {
await dcClicarValidadoLinha(id);
} catch (e) {
await pidAlert(e.message);
} finally {
validadoBtn.disabled = false;
}
return;
}
const obsBtn = event.target.closest("[data-dc-dre-observacao]");
if (obsBtn) {
const id = Number(obsBtn.getAttribute("data-dc-dre-observacao"));
dcDreObsEditId = dcDreObsEditId === id ? null : id;
renderDre();
return;
}
const cancelBtn = event.target.closest("[data-dc-dre-obs-cancel]");
if (cancelBtn) {
dcDreObsEditId = null;
renderDre();
return;
}
const saveBtn = event.target.closest("[data-dc-dre-obs-save]");
if (saveBtn) {
const id = Number(saveBtn.getAttribute("data-dc-dre-obs-save"));
const errorEl = document.getElementById(`dc-dre-obs-error-${id}`);
errorEl.textContent = "";
const texto = document.getElementById(`dc-dre-obs-texto-${id}`).value;
const mostrar = document.getElementById(`dc-dre-obs-mostrar-${id}`).checked;
saveBtn.disabled = true;
try {
const atualizada = await pidAtualizarObservacaoLinhaDreContabil(id, {
observacao: texto,
oculta_no_relatorio: !mostrar,
});
const indice = apuracaoAtual.linhas_dre.findIndex((l) => l.id === id);
apuracaoAtual.linhas_dre[indice] = atualizada;
dcDreObsEditId = null;
renderDre();
} catch (e) {
errorEl.textContent = e.message;
saveBtn.disabled = false;
}
}
});
// Cabeçalho de coluna da Análise Vertical é montado em JS (não hardcoded
// no HTML) porque o número de meses varia conforme o período do PDF
// (normalmente 3, ver ContabilApuracao.analise_vertical_meses) — cada mês
// vira duas colunas (Valor/Variação).
function renderAnaliseVerticalHead() {
const meses = apuracaoAtual.analise_vertical_meses || [];
const row = document.getElementById("dc-av-head-row");
row.innerHTML = `
<th>Descrição</th>
${meses
.map((mes) => {
const label = pidDcEscapeHtml(pidDcFormatMesAnaliseVertical(mes));
return `<th>${label} — Valor</th><th>${label} — Variação</th>`;
})
.join("")}
<th>Observação</th>
`;
}
// Mesma árvore recolhível do Balancete/DRE (ver renderContas/renderDre),
// agora sobre ContabilLinhaAnaliseVertical — cada linha mostra um
// valor+percentual por mês (colunas dinâmicas) em vez de um valor único.
function renderAnaliseVertical() {
renderAnaliseVerticalHead();
const body = document.getElementById("dc-av-body");
body.innerHTML = "";
const concluida = apuracaoAtual.status === "concluida";
const linhas = apuracaoAtual.linhas_analise_vertical;
const colspan = 2 + (apuracaoAtual.analise_vertical_meses || []).length * 2;
const nivelFn = (l) => Math.max(0, l.nivel);
const niveis = linhas.map(nivelFn);
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] && dcAvColapsadas.has(linha.id)) {
pilhaColapso.push(nivel);
}
if (oculta) return;
const expandida = !dcAvColapsadas.has(linha.id);
const toggle = temFilhos[i]
? `<button type="button" class="dc-conta-toggle${expandida ? " is-expanded" : ""}" data-dc-av-toggle="${linha.id}" aria-label="${expandida ? "Recolher" : "Expandir"} ${pidDcEscapeHtml(linha.descricao)}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>`
: `<span class="dc-conta-toggle-spacer"></span>`;
const validadoInfo = dcValidadoInfo(linha, linhas, nivelFn, temFilhos[i]);
const colunasMensais = linha.valores
.map(
(v) =>
`<td>${pidDcFormatMoeda(v.valor)}</td><td>${pidDcFormatPercentualAnaliseVertical(v.percentual)}</td>`
)
.join("");
const tr = document.createElement("tr");
tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row";
if (dcAvDestaque.has(linha.id)) tr.classList.add("dc-conta-row--destaque");
const indentacao = nivel * 16;
tr.innerHTML = `
<td style="padding-left: calc(var(--space-4) + ${indentacao}px)">
<span class="dc-conta-desc-cell">${toggle}<span>${pidDcEscapeHtml(linha.descricao)}</span></span>
</td>
${colunasMensais}
<td>
<div class="dc-obs-cell-actions">
<button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-av-validado="${linha.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}>
${PID_DC_VALIDADO_ICONE}
</button>
<button type="button" class="dc-conta-observacao-btn${linha.observacao ? " dc-conta-observacao-btn--preenchida" : ""}" data-dc-av-observacao="${linha.id}" title="${pidDcEscapeHtml(linha.observacao || "Adicionar observação")}" aria-label="${linha.observacao ? "Editar observação" : "Adicionar observação"}">
${PID_DC_OBSERVACAO_ICONE}
</button>
</div>
</td>
`;
body.appendChild(tr);
if (dcAvObsEditId === linha.id) {
const editRow = document.createElement("tr");
editRow.className = "dc-obs-edit-row";
editRow.innerHTML = `
<td colspan="${colspan}">
<div class="dc-obs-edit">
<div class="modal-field">
<textarea id="dc-av-obs-texto-${linha.id}" rows="3" placeholder="Anote sua análise..." ${concluida ? "disabled" : ""}>${pidDcEscapeHtml(linha.observacao || "")}</textarea>
</div>
<label class="modal-checkbox">
<input type="checkbox" id="dc-av-obs-mostrar-${linha.id}" ${linha.observacao && !linha.oculta_no_relatorio ? "checked" : ""} ${concluida ? "disabled" : ""} />
Mostrar esta observação ao cliente no relatório
</label>
<p class="modal-error" id="dc-av-obs-error-${linha.id}"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" data-dc-av-obs-cancel="${linha.id}">${concluida ? "Fechar" : "Cancelar"}</button>
${concluida ? "" : `<button type="button" class="btn-solid" data-dc-av-obs-save="${linha.id}">Salvar</button>`}
</div>
</div>
</td>
`;
body.appendChild(editRow);
}
});
renderAnaliseVerticalObsResumo();
}
document.getElementById("dc-av-body").addEventListener("click", async (event) => {
const toggleBtn = event.target.closest("[data-dc-av-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-av-toggle"));
const estavaColapsada = dcAvColapsadas.has(id);
if (estavaColapsada) dcAvColapsadas.delete(id);
else dcAvColapsadas.add(id);
const nivelFn = (linha) => Math.max(0, linha.nivel);
if (estavaColapsada) {
dcAvDestaqueHistorico.push(dcAvDestaque);
dcAvDestaque = dcFilhosDiretos(apuracaoAtual.linhas_analise_vertical, nivelFn, id);
} else {
dcAvDestaque = dcAvDestaqueHistorico.pop() || new Set();
}
renderAnaliseVertical();
return;
}
const validadoBtn = event.target.closest("[data-dc-av-validado]");
if (validadoBtn) {
if (validadoBtn.disabled) return;
const id = Number(validadoBtn.getAttribute("data-dc-av-validado"));
validadoBtn.disabled = true;
try {
await dcClicarValidadoAv(id);
} catch (e) {
await pidAlert(e.message);
} finally {
validadoBtn.disabled = false;
}
return;
}
const obsBtn = event.target.closest("[data-dc-av-observacao]");
if (obsBtn) {
const id = Number(obsBtn.getAttribute("data-dc-av-observacao"));
dcAvObsEditId = dcAvObsEditId === id ? null : id;
renderAnaliseVertical();
return;
}
const cancelBtn = event.target.closest("[data-dc-av-obs-cancel]");
if (cancelBtn) {
dcAvObsEditId = null;
renderAnaliseVertical();
return;
}
const saveBtn = event.target.closest("[data-dc-av-obs-save]");
if (saveBtn) {
const id = Number(saveBtn.getAttribute("data-dc-av-obs-save"));
const errorEl = document.getElementById(`dc-av-obs-error-${id}`);
errorEl.textContent = "";
const texto = document.getElementById(`dc-av-obs-texto-${id}`).value;
const mostrar = document.getElementById(`dc-av-obs-mostrar-${id}`).checked;
saveBtn.disabled = true;
try {
const atualizada = await pidAtualizarObservacaoLinhaAnaliseVerticalContabil(id, {
observacao: texto,
oculta_no_relatorio: !mostrar,
});
const indice = apuracaoAtual.linhas_analise_vertical.findIndex((l) => l.id === id);
apuracaoAtual.linhas_analise_vertical[indice] = atualizada;
dcAvObsEditId = null;
renderAnaliseVertical();
} catch (e) {
errorEl.textContent = e.message;
saveBtn.disabled = false;
}
}
});
// ---- 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();
});
// ---- Resumo do Fechamento (texto rico, aba "Dashboard") ----
// Editor contenteditable + colar/arrastar imagem embutida — mesmo padrão
// de `.ag-richtext` (acessos-gerais.js) e `.ajuda-modal__editor`
// (ajuda-aplicacao.js), duplicado aqui de propósito (não é um componente
// compartilhado no projeto). Populado só em `renderRevisao()` (uma vez
// por apuração aberta), nunca em `renderDashboardTab()` — que roda toda
// vez que o contador troca de aba, e resetar o `innerHTML` a cada troca
// descartaria texto ainda não salvo.
const PID_DC_RESUMO_FECHAMENTO_IMAGE_MAX_BYTES = 2 * 1024 * 1024;
const resumoFechamentoEditor = document.getElementById("dc-dash-resumo-fechamento-editor");
const resumoFechamentoSalvarBtn = document.getElementById("dc-dash-resumo-fechamento-salvar-btn");
const resumoFechamentoStatusEl = document.getElementById("dc-dash-resumo-fechamento-status");
function renderResumoFechamento() {
const concluida = apuracaoAtual.status === "concluida";
resumoFechamentoEditor.innerHTML = apuracaoAtual.resumo_fechamento || "";
resumoFechamentoEditor.setAttribute("contenteditable", concluida ? "false" : "true");
resumoFechamentoSalvarBtn.hidden = concluida;
resumoFechamentoStatusEl.hidden = true;
}
function pidDcResumoFechamentoPlaceCaretAtPoint(x, y) {
const selection = window.getSelection();
if (document.caretRangeFromPoint) {
const range = document.caretRangeFromPoint(x, y);
if (range) {
selection.removeAllRanges();
selection.addRange(range);
}
} else if (document.caretPositionFromPoint) {
const pos = document.caretPositionFromPoint(x, y);
if (pos) {
const range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
}
}
function pidDcResumoFechamentoMostraStatus(texto) {
resumoFechamentoStatusEl.hidden = false;
resumoFechamentoStatusEl.textContent = texto;
}
function pidDcResumoFechamentoInserirImagem(file, atPoint) {
if (!file || !file.type.startsWith("image/")) return;
if (file.size > PID_DC_RESUMO_FECHAMENTO_IMAGE_MAX_BYTES) {
pidDcResumoFechamentoMostraStatus("A imagem deve ter no máximo 2MB.");
return;
}
const reader = new FileReader();
reader.onload = () => {
resumoFechamentoEditor.focus();
if (atPoint) pidDcResumoFechamentoPlaceCaretAtPoint(atPoint.x, atPoint.y);
document.execCommand("insertImage", false, reader.result);
};
reader.readAsDataURL(file);
}
resumoFechamentoEditor.addEventListener("paste", (event) => {
const items = Array.from((event.clipboardData && event.clipboardData.items) || []);
const imageItem = items.find((item) => item.type.startsWith("image/"));
if (!imageItem) return;
event.preventDefault();
pidDcResumoFechamentoInserirImagem(imageItem.getAsFile());
});
resumoFechamentoEditor.addEventListener("dragover", (event) => {
event.preventDefault();
});
resumoFechamentoEditor.addEventListener("drop", (event) => {
const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
if (!file || !file.type.startsWith("image/")) return;
event.preventDefault();
pidDcResumoFechamentoInserirImagem(file, { x: event.clientX, y: event.clientY });
});
resumoFechamentoSalvarBtn.addEventListener("click", async () => {
const hasImage = resumoFechamentoEditor.querySelector("img") !== null;
const hasText = resumoFechamentoEditor.textContent.trim() !== "";
const valor = hasImage || hasText ? resumoFechamentoEditor.innerHTML : "";
resumoFechamentoSalvarBtn.disabled = true;
try {
const resultado = await pidAtualizarResumoFechamentoContabil(apuracaoAtual.id, valor);
apuracaoAtual.resumo_fechamento = resultado.resumo_fechamento;
pidDcResumoFechamentoMostraStatus("Salvo.");
} catch (e) {
pidDcResumoFechamentoMostraStatus(e.message);
} finally {
resumoFechamentoSalvarBtn.disabled = false;
}
});
// ---- 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 = `
<h4 class="dc-dash-grupo__titulo">${pidDcEscapeHtml(nomeGrupo)}</h4>
<div class="dc-dash-cards-grid"></div>
`;
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 = `
<div class="dc-dash-card__info">
<span class="dc-dash-card__label">${pidDcEscapeHtml(meta.nome)}</span>
<span class="dc-dash-card__valor">${pidDcFormatIndicadorGenerico(meta.formato, indicadores[chave])}</span>
</div>
<div class="dc-dash-card__actions">
<button
type="button"
class="icon-btn dc-dash-toggle-btn"
data-dc-dash-indicador-editar="${meta.definicao_id}"
aria-label="Editar indicador"
title="Editar indicador"
${concluida ? "disabled" : ""}
>${PID_DC_ICON_EDITAR}</button>
<button
type="button"
class="icon-btn dc-dash-toggle-btn"
data-dc-dash-indicador-toggle="${chave}"
aria-label="${oculto ? "Mostrar" : "Ocultar"} no relatório"
title="${oculto ? "Mostrar no relatório" : "Ocultar do relatório"}"
${concluida ? "disabled" : ""}
>${oculto ? PID_DC_ICON_OLHO_FECHADO : PID_DC_ICON_OLHO_ABERTO}</button>
</div>
`;
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: '<path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/>' },
{
chave: "tendencia-alta",
rotulo: "Tendência de alta",
svg: '<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>',
},
{
chave: "tendencia-baixa",
rotulo: "Tendência de baixa",
svg: '<polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/>',
},
{
chave: "percentual",
rotulo: "Percentual",
svg: '<line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/>',
},
{
chave: "pizza",
rotulo: "Gráfico de pizza",
svg: '<path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/>',
},
{ chave: "atividade", rotulo: "Atividade", svg: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>' },
{
chave: "moeda",
rotulo: "Cifrão",
svg: '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>',
},
{
chave: "alvo",
rotulo: "Alvo/meta",
svg: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
},
{
chave: "camadas",
rotulo: "Camadas",
svg:
'<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/>' +
'<polyline points="2 12 12 17 22 12"/>',
},
{
chave: "cartao",
rotulo: "Cartão",
svg: '<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>',
},
{
chave: "selo",
rotulo: "Selo/prêmio",
svg: '<circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/>',
},
];
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 = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${icone.svg}</svg>`;
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 = `
<input type="text" class="checklist-search" placeholder="Buscar conta por código ou descrição..." data-dc-comp-busca />
<div class="checklist-box" data-dc-comp-lista>
${orfaos.map((codigo) => `
<label class="checklist-item" data-dc-comp-item="${pidDcEscapeHtml(codigo.toLowerCase())}">
<input type="checkbox" value="${pidDcEscapeHtml(codigo)}" checked />
<span class="checklist-item__info"><span>${pidDcEscapeHtml(codigo)} — <em>conta não encontrada nesta apuração, mantida</em></span></span>
</label>
`).join("")}
${apuracaoAtual.contas.map((conta) => `
<label class="checklist-item" data-dc-comp-item="${pidDcEscapeHtml(`${conta.codigo} ${conta.descricao}`.toLowerCase())}">
<input type="checkbox" value="${pidDcEscapeHtml(conta.codigo)}" ${selecionadosSet.has(conta.codigo) ? "checked" : ""} />
<span class="checklist-item__info"><span>${pidDcEscapeHtml(conta.codigo)} — ${pidDcEscapeHtml(conta.descricao)}</span></span>
</label>
`).join("")}
</div>
`;
} 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 = `
<input type="text" class="checklist-search" placeholder="Buscar linha da DRE..." data-dc-comp-busca />
<div class="checklist-box" data-dc-comp-lista>
${orfaos.map((descricao) => `
<label class="checklist-item" data-dc-comp-item="${pidDcEscapeHtml(descricao.toLowerCase())}">
<input type="checkbox" value="${pidDcEscapeHtml(descricao)}" checked />
<span class="checklist-item__info"><span>${pidDcEscapeHtml(descricao)} — <em>linha não encontrada nesta apuração, mantida</em></span></span>
</label>
`).join("")}
${apuracaoAtual.linhas_dre.map((linha) => `
<label class="checklist-item" data-dc-comp-item="${pidDcEscapeHtml(linha.descricao.toLowerCase())}">
<input type="checkbox" value="${pidDcEscapeHtml(linha.descricao)}" ${selecionadosSet.has(linha.descricao) ? "checked" : ""} />
<span class="checklist-item__info"><span>${pidDcEscapeHtml(linha.descricao)}</span></span>
</label>
`).join("")}
</div>
`;
} 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 = `
<select class="dc-ind-comp-indicador">
<option value="">Selecione um indicador...</option>
${opcoes.map((chave) => `<option value="${pidDcEscapeHtml(chave)}" ${chave === atual ? "selected" : ""}>${pidDcEscapeHtml(metadados[chave].nome)}</option>`).join("")}
</select>
`;
} 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 = `<p class="dc-ind-formula-hint">Usa automaticamente o resultado líquido do período (última linha da DRE desta apuração), sem precisar escolher nada aqui.</p>`;
} 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 = `
<div class="dc-ind-componente__linha">
<div class="modal-field">
<label>Chave (usada na fórmula)</label>
<input type="text" class="dc-ind-comp-chave" placeholder="ex.: receita" value="${pidDcEscapeHtml(valoresIniciais.chave || "")}" />
</div>
<div class="modal-field">
<label>Tipo</label>
<select class="dc-ind-comp-tipo">
${Object.keys(PID_DC_COMP_TIPO_LABELS).map((tipo) => `<option value="${tipo}" ${tipo === tipoInicial ? "selected" : ""}>${PID_DC_COMP_TIPO_LABELS[tipo]}</option>`).join("")}
</select>
</div>
<button type="button" class="icon-btn dc-ind-comp-remover" aria-label="Remover componente" title="Remover componente">${PID_DC_ICON_LIXEIRA}</button>
</div>
<div class="dc-ind-comp-picker"></div>
`;
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 = `<p class="dc-ind-preview__erro">Preencha a chave de todos os componentes e a fórmula antes de calcular.</p>`;
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 = `<p class="dc-ind-preview__carregando">Calculando...</p>`;
try {
const resultado = await pidPreVisualizarIndicadorContabil(apuracaoAtual.id, payload);
if (resultado.erro) {
previewEl.innerHTML = `<p class="dc-ind-preview__erro">${pidDcEscapeHtml(resultado.erro)}</p>`;
} else {
previewEl.innerHTML = `
<ul class="dc-ind-preview__lista">
${resultado.componentes.map((c) => `
<li><span>${pidDcEscapeHtml(c.chave)}</span><span>${c.valor_formatado === null ? "—" : pidDcEscapeHtml(c.valor_formatado)}</span></li>
`).join("")}
</ul>
<div class="dc-ind-preview__resultado">Resultado: <strong>${resultado.resultado_formatado === null ? "—" : pidDcEscapeHtml(resultado.resultado_formatado)}</strong></div>
`;
}
} catch (e) {
previewEl.innerHTML = `<p class="dc-ind-preview__erro">${pidDcEscapeHtml(e.message)}</p>`;
} 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
? `<span class="dc-ind-hub-item__spacer"></span>`
: `<input
type="checkbox"
class="dc-ind-hub-item__ativo"
data-dc-hub-toggle="${definicao.chave}"
${ativo ? "checked" : ""}
${concluida ? "disabled" : ""}
aria-label="Ativo nesta apuração"
title="Ativo nesta apuração"
/>`
}
<button type="button" class="dc-ind-hub-item__nome" data-dc-hub-ver="${definicao.id}">${pidDcEscapeHtml(definicao.nome)}</button>
<button
type="button"
class="icon-btn"
data-dc-hub-editar="${definicao.id}"
aria-label="Editar"
title="Editar"
${concluida ? "disabled" : ""}
>${PID_DC_ICON_EDITAR}</button>
`;
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 = `
<div class="dc-dash-obs__info">
<span class="dc-dash-obs__origem">${pidDcEscapeHtml(item.origem)}</span>
<span class="dc-dash-obs__referencia">${pidDcEscapeHtml(item.referencia)}</span>
<p class="dc-dash-obs__texto">${pidDcEscapeHtml(item.texto)}</p>
</div>
<button
type="button"
class="icon-btn dc-dash-toggle-btn"
data-dc-dash-obs-toggle="${item.tipo}:${item.id}"
aria-label="${item.oculto ? "Mostrar" : "Ocultar"} no relatório"
title="${item.oculto ? "Mostrar no relatório" : "Ocultar do relatório"}"
${concluida ? "disabled" : ""}
>
${item.oculto ? PID_DC_ICON_OLHO_FECHADO : PID_DC_ICON_OLHO_ABERTO}
</button>
`;
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);
}
});
// ---- Resumo de observações no final do Balancete/DRE ----
// Pedido explícito do usuário: além da lista combinada (Balancete+DRE+
// Auditoria) que já existia na aba "Dashboard", cada aba (Balancete/DRE)
// ganha seu próprio resumo — quantidade + as observações já registradas
// ali, com o mesmo botão de olho pra mostrar/ocultar do relatório do
// cliente, sem precisar trocar de aba pra isso. Reaproveita a marcação
// visual `.dc-dash-obs*` já usada pela lista combinada.
function pidDcObsItemHtml(item, concluida, toggleAttr) {
return `
<div class="dc-dash-obs${item.oculto ? " is-oculto" : ""}">
<div class="dc-dash-obs__info">
<span class="dc-dash-obs__referencia">${pidDcEscapeHtml(item.referencia)}</span>
<p class="dc-dash-obs__texto">${pidDcEscapeHtml(item.texto)}</p>
</div>
<button
type="button"
class="icon-btn dc-dash-toggle-btn"
${toggleAttr}="${item.id}"
aria-label="${item.oculto ? "Mostrar" : "Ocultar"} no relatório"
title="${item.oculto ? "Mostrar no relatório" : "Ocultar do relatório"}"
${concluida ? "disabled" : ""}
>
${item.oculto ? PID_DC_ICON_OLHO_FECHADO : PID_DC_ICON_OLHO_ABERTO}
</button>
</div>
`;
}
function renderContasObsResumo() {
const lista = document.getElementById("dc-contas-obs-list");
const empty = document.getElementById("dc-contas-obs-empty");
const count = document.getElementById("dc-contas-obs-count");
const concluida = apuracaoAtual.status === "concluida";
const itens = apuracaoAtual.contas.filter((conta) => conta.observacao);
count.textContent = String(itens.length);
empty.hidden = itens.length > 0;
lista.innerHTML = itens
.map((conta) =>
pidDcObsItemHtml(
{ id: conta.id, referencia: `${conta.codigo} — ${conta.descricao}`, texto: conta.observacao, oculto: conta.oculta_no_relatorio },
concluida,
"data-dc-contas-resumo-toggle"
)
)
.join("");
}
document.getElementById("dc-contas-obs-list").addEventListener("click", async (event) => {
const btn = event.target.closest("[data-dc-contas-resumo-toggle]");
if (!btn || btn.disabled) return;
const id = Number(btn.getAttribute("data-dc-contas-resumo-toggle"));
const conta = apuracaoAtual.contas.find((c) => c.id === id);
btn.disabled = true;
try {
const atualizada = await pidAlternarOcultoContaContabil(id, !conta.oculta_no_relatorio);
const indice = apuracaoAtual.contas.findIndex((c) => c.id === id);
apuracaoAtual.contas[indice] = atualizada;
renderContasObsResumo();
} catch (e) {
btn.disabled = false;
await pidAlert(e.message);
}
});
function renderDreObsResumo() {
const lista = document.getElementById("dc-dre-obs-list");
const empty = document.getElementById("dc-dre-obs-empty");
const count = document.getElementById("dc-dre-obs-count");
const concluida = apuracaoAtual.status === "concluida";
const itens = apuracaoAtual.linhas_dre.filter((linha) => linha.observacao);
count.textContent = String(itens.length);
empty.hidden = itens.length > 0;
lista.innerHTML = itens
.map((linha) =>
pidDcObsItemHtml(
{ id: linha.id, referencia: linha.descricao, texto: linha.observacao, oculto: linha.oculta_no_relatorio },
concluida,
"data-dc-dre-resumo-toggle"
)
)
.join("");
}
document.getElementById("dc-dre-obs-list").addEventListener("click", async (event) => {
const btn = event.target.closest("[data-dc-dre-resumo-toggle]");
if (!btn || btn.disabled) return;
const id = Number(btn.getAttribute("data-dc-dre-resumo-toggle"));
const linha = apuracaoAtual.linhas_dre.find((l) => l.id === id);
btn.disabled = true;
try {
const atualizada = await pidAlternarOcultoLinhaDreContabil(id, !linha.oculta_no_relatorio);
const indice = apuracaoAtual.linhas_dre.findIndex((l) => l.id === id);
apuracaoAtual.linhas_dre[indice] = atualizada;
renderDreObsResumo();
} catch (e) {
btn.disabled = false;
await pidAlert(e.message);
}
});
function renderAnaliseVerticalObsResumo() {
const lista = document.getElementById("dc-av-obs-list");
const empty = document.getElementById("dc-av-obs-empty");
const count = document.getElementById("dc-av-obs-count");
const concluida = apuracaoAtual.status === "concluida";
const itens = apuracaoAtual.linhas_analise_vertical.filter((linha) => linha.observacao);
count.textContent = String(itens.length);
empty.hidden = itens.length > 0;
lista.innerHTML = itens
.map((linha) =>
pidDcObsItemHtml(
{ id: linha.id, referencia: linha.descricao, texto: linha.observacao, oculto: linha.oculta_no_relatorio },
concluida,
"data-dc-av-resumo-toggle"
)
)
.join("");
}
document.getElementById("dc-av-obs-list").addEventListener("click", async (event) => {
const btn = event.target.closest("[data-dc-av-resumo-toggle]");
if (!btn || btn.disabled) return;
const id = Number(btn.getAttribute("data-dc-av-resumo-toggle"));
const linha = apuracaoAtual.linhas_analise_vertical.find((l) => l.id === id);
btn.disabled = true;
try {
const atualizada = await pidAlternarOcultoLinhaAnaliseVerticalContabil(id, !linha.oculta_no_relatorio);
const indice = apuracaoAtual.linhas_analise_vertical.findIndex((l) => l.id === id);
apuracaoAtual.linhas_analise_vertical[indice] = atualizada;
renderAnaliseVerticalObsResumo();
} catch (e) {
btn.disabled = false;
await pidAlert(e.message);
}
});
// ---- Modal de achado (tratar/ignorar) ----
const achadoModal = document.getElementById("dc-achado-modal");
function abrirModalAchado(achadoId) {
const achado = apuracaoAtual.achados.find((a) => a.id === achadoId);
if (!achado) return;
achadoEmEdicao = achadoId;
document.getElementById("dc-achado-modal-titulo").textContent = achado.titulo;
document.getElementById("dc-achado-modal-mensagem").textContent = achado.mensagem;
document.getElementById("dc-achado-modal-texto").value = achado.observacao_contador || "";
document.getElementById("dc-achado-modal-error").textContent = "";
achadoModal.hidden = false;
}
function fecharModalAchado() {
achadoModal.hidden = true;
achadoEmEdicao = null;
}
document.getElementById("dc-achado-modal-cancel-btn").addEventListener("click", fecharModalAchado);
achadoModal.addEventListener("click", (event) => {
if (event.target === achadoModal) fecharModalAchado();
});
async function salvarStatusAchado(novoStatus) {
const errorEl = document.getElementById("dc-achado-modal-error");
errorEl.textContent = "";
const texto = document.getElementById("dc-achado-modal-texto").value.trim();
if (!texto) {
errorEl.textContent = "Informe uma justificativa para o tratamento desta observação.";
return;
}
try {
const achado = await pidAtualizarAchadoContabil(achadoEmEdicao, novoStatus, texto);
const indice = apuracaoAtual.achados.findIndex((a) => a.id === achadoEmEdicao);
if (indice >= 0) apuracaoAtual.achados[indice] = achado;
renderAchados();
fecharModalAchado();
await carregarLista();
} catch (e) {
errorEl.textContent = e.message;
}
}
document.getElementById("dc-achado-modal-tratar-btn").addEventListener("click", () => salvarStatusAchado("tratado"));
document.getElementById("dc-achado-modal-ignorar-btn").addEventListener("click", () => salvarStatusAchado("ignorado"));
// ---- Concluir análise ----
document.getElementById("dc-concluir-btn").addEventListener("click", async () => {
if (!(await pidConfirm("Concluir esta análise? Depois de concluída, as observações não podem mais ser editadas.")))
return;
apuracaoAtual = await pidConcluirApuracaoContabil(apuracaoAtual.id);
renderRevisao();
await carregarLista();
});
document.getElementById("dc-review-back-btn").addEventListener("click", () => showView("list"));
// ---- Gerar Dashboard ----
document.getElementById("dc-gerar-dashboard-btn").addEventListener("click", () => {
pidGerarDashboardContabil(apuracaoAtual.id);
});
await carregarLista();
showView("list");
});