portal_publico/static/js/dashboard-contabil.js

1684 lines
74 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>';
function pidDcFormatCompetencia(iso) {
if (!iso) return "";
const [ano, mes] = iso.split("-");
return `${mes}/${ano}`;
}
function pidDcFormatData(iso) {
if (!iso) return "";
return new Date(iso).toLocaleDateString("pt-BR");
}
function pidDcEscapeHtml(texto) {
const div = document.createElement("div");
div.textContent = texto == null ? "" : String(texto);
return div.innerHTML;
}
async function pidFetchApuracoesContabil() {
return pidApiRequest("/contabil-apuracoes/");
}
async function pidFetchApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/`);
}
async function pidCriarApuracaoContabil(formData) {
return pidApiRequest("/contabil-apuracoes/", { method: "POST", body: formData });
}
async function pidExcluirApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/`, { method: "DELETE" });
}
async function pidConcluirApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/concluir/`, { method: "POST" });
}
/**
* /dashboard/ é GET (não POST, diferente do padrão /gerar/ de outras
* ferramentas) exatamente pra poder ser aberto assim — navegação de
* verdade numa nova aba, não um blob em memória. Um blob criado via fetch
* (`URL.createObjectURL`) tem sua própria origem sintética: URLs relativas
* dentro do HTML (como `{% static %}` da logo do escritório, que gera
* "/static/...") não resolvem de forma confiável contra a origem do Portal
* dentro de um documento carregado de um `blob:`, quebrando a imagem — com
* navegação de verdade pra URL da API, isso deixa de ser um problema.
*/
function pidGerarDashboardContabil(id) {
window.open(`/api/contabil-apuracoes/${id}/dashboard/`, "_blank");
}
async function pidAtualizarObservacaoContaContabil(id, observacao) {
return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: { observacao } });
}
async function pidAtualizarObservacaoLinhaDreContabil(id, observacao) {
return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: { observacao } });
}
async function pidAtualizarAchadoContabil(id, status, observacaoContador) {
return pidApiRequest(`/contabil-achados/${id}/`, {
method: "PATCH",
body: { status, observacao_contador: observacaoContador },
});
}
// ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ----
async function pidBuscarIndicadoresContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores/`);
}
async function pidAtualizarIndicadoresOcultosContabil(id, indicadoresOcultos) {
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-ocultos/`, {
method: "POST",
body: { indicadores_ocultos: indicadoresOcultos },
});
}
async function pidAlternarOcultoContaContabil(id, oculto) {
return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: { oculta_no_relatorio: oculto } });
}
async function pidAlternarOcultoLinhaDreContabil(id, oculto) {
return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: { oculta_no_relatorio: oculto } });
}
async function pidAlternarOcultoAchadoContabil(id, oculto) {
return pidApiRequest(`/contabil-achados/${id}/alternar-oculto/`, {
method: "POST",
body: { oculto_no_relatorio: oculto },
});
}
async function pidBuscarIndicadorContabilDefinicao(id) {
return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`);
}
async function pidCriarIndicadorContabil(payload) {
return pidApiRequest("/contabil-indicadores-definicoes/", { method: "POST", body: payload });
}
async function pidAtualizarIndicadorContabil(id, payload) {
return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`, { method: "PATCH", body: payload });
}
async function pidExcluirIndicadorContabil(id) {
return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`, { method: "DELETE" });
}
async function pidBuscarDefinicoesIndicadoresContabil() {
return pidApiRequest("/contabil-indicadores-definicoes/");
}
async function pidPreVisualizarIndicadorContabil(apuracaoId, payload) {
return pidApiRequest(`/contabil-apuracoes/${apuracaoId}/pre-visualizar-indicador/`, {
method: "POST",
body: payload,
});
}
async function pidAtualizarIndicadoresSelecionadosContabil(id, indicadoresSelecionados) {
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-selecionados/`, {
method: "POST",
body: { indicadores_selecionados: indicadoresSelecionados },
});
}
document.addEventListener("DOMContentLoaded", async () => {
const listView = document.getElementById("dc-list-view");
if (!listView) return;
const me = await pidGetMe();
if (!me) return;
const relatoriosPerms = (me.permissoes_efetivas && me.permissoes_efetivas["relatorios"]) || {};
const canUse = !!(relatoriosPerms.enabled && relatoriosPerms.apps && relatoriosPerms.apps["dashboard-contabil"]);
const noAccessEl = document.getElementById("dc-no-access");
const contentEl = document.getElementById("dc-content");
if (noAccessEl) noAccessEl.hidden = canUse;
if (contentEl) contentEl.hidden = !canUse;
if (!canUse) return;
const formView = document.getElementById("dc-form-view");
const reviewView = document.getElementById("dc-review-view");
let apuracaoAtual = null;
let filtroSeveridade = "todas";
let filtroStatusAchado = "pendente";
// Categoria (achado.regra) selecionada clicando num card do resumo — null
// = nenhuma categoria isolada. Filtro independente de severidade/status,
// os três se combinam (E lógico) na lista detalhada abaixo do resumo.
let filtroRegra = null;
let achadoEmEdicao = null;
// Ids de ContabilConta com filhos recolhidos na árvore do Balancete —
// reiniciado toda vez que uma apuração é aberta/criada (ver renderRevisao).
let dcContasColapsadas = new Set();
// Mesma ideia, mas para a árvore da DRE (ContabilLinhaDre.id) — nível de
// indentação já vem pronto do backend (linha.nivel), sem precisar derivar
// de um código de classificação como no Balancete.
let dcDreColapsadas = new Set();
// Ids de ContabilAchado com o detalhe da conta usada no apontamento
// expandido (ver renderAchados) — só achados com `conta` preenchido (as
// regras gerais, ex. balanceamento Ativo x Passivo, não têm uma conta
// única por trás, então não ganham o toggle).
let dcAchadosContaExpandida = new Set();
// { indicadores, indicadores_ocultos } da aba "Dashboard" — carregado sob
// demanda (só quando essa aba é aberta pela primeira vez após abrir/criar
// a apuração, ver renderDashboardTab), null até lá.
let dcIndicadoresAtual = null;
function showView(view) {
listView.hidden = view !== "list";
formView.hidden = view !== "form";
reviewView.hidden = view !== "review";
}
// ---- Histórico ----
async function carregarLista() {
const apuracoes = await pidFetchApuracoesContabil();
const body = document.getElementById("dc-list-body");
const empty = document.getElementById("dc-list-empty");
body.innerHTML = "";
empty.hidden = apuracoes.length > 0;
apuracoes.forEach((apuracao) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${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.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));
dcContasDestaque = new Set();
dcDreDestaque = new Set();
dcAchadosContaExpandida = new Set();
dcIndicadoresAtual = null;
renderAchados();
renderContas();
renderDre();
}
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;
}
// 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; um clique de recolher só
// limpa. Um por árvore, já que Balancete e DRE são independentes.
let dcContasDestaque = new Set();
let dcDreDestaque = new Set();
// 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;
}
function renderContas() {
const body = document.getElementById("dc-contas-body");
body.innerHTML = "";
const contas = apuracaoAtual.contas;
const niveis = contas.map(dcContaNivel);
const temFilhos = contas.map((_, i) => i + 1 < contas.length && niveis[i + 1] > niveis[i]);
// Pilha dos níveis de ancestrais recolhidos — enquanto uma linha tiver
// algum ancestral na pilha, ela fica oculta (é descendente de um grupo
// fechado), mesmo padrão de renderização de árvore a partir de uma
// lista plana com profundidade já conhecida.
const pilhaColapso = [];
contas.forEach((conta, i) => {
const nivel = niveis[i];
while (pilhaColapso.length && pilhaColapso[pilhaColapso.length - 1] >= nivel) {
pilhaColapso.pop();
}
const oculta = pilhaColapso.length > 0;
if (temFilhos[i] && dcContasColapsadas.has(conta.id)) {
pilhaColapso.push(nivel);
}
if (oculta) return;
const expandida = !dcContasColapsadas.has(conta.id);
const toggle = temFilhos[i]
? `<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 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>
<button type="button" class="dc-conta-observacao-btn" data-dc-conta-observacao="${conta.id}" title="${pidDcEscapeHtml(conta.observacao || "Adicionar observação")}">
${conta.observacao ? pidDcEscapeHtml(conta.observacao) : "+ Observação"}
</button>
</td>
`;
body.appendChild(tr);
});
}
document.getElementById("dc-contas-body").addEventListener("click", (event) => {
const toggleBtn = event.target.closest("[data-dc-conta-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-conta-toggle"));
const estavaColapsada = dcContasColapsadas.has(id);
if (estavaColapsada) dcContasColapsadas.delete(id);
else dcContasColapsadas.add(id);
// Expandir troca o destaque pros filhos recém-revelados; recolher só
// limpa (nada novo foi revelado) — ver dcContasDestaque acima.
dcContasDestaque = estavaColapsada ? dcFilhosDiretos(apuracaoAtual.contas, dcContaNivel, id) : new Set();
renderContas();
return;
}
const btn = event.target.closest("[data-dc-conta-observacao]");
if (!btn) return;
abrirModalObservacao("conta", Number(btn.getAttribute("data-dc-conta-observacao")));
});
// Mesma árvore recolhível do Balancete (ver renderContas), agora sobre
// ContabilLinhaDre.nivel — que já chega pronto do backend na ordem de
// leitura do PDF, então "ter filhos" é só olhar se a próxima linha tem
// nível maior que o da linha atual.
function renderDre() {
const body = document.getElementById("dc-dre-body");
body.innerHTML = "";
const linhas = apuracaoAtual.linhas_dre;
const niveis = linhas.map((linha) => Math.max(0, linha.nivel));
const temFilhos = linhas.map((_, i) => i + 1 < linhas.length && niveis[i + 1] > niveis[i]);
const pilhaColapso = [];
linhas.forEach((linha, i) => {
const nivel = niveis[i];
while (pilhaColapso.length && pilhaColapso[pilhaColapso.length - 1] >= nivel) {
pilhaColapso.pop();
}
const oculta = pilhaColapso.length > 0;
if (temFilhos[i] && dcDreColapsadas.has(linha.id)) {
pilhaColapso.push(nivel);
}
if (oculta) return;
const expandida = !dcDreColapsadas.has(linha.id);
const toggle = temFilhos[i]
? `<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 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>
<button type="button" class="dc-conta-observacao-btn" data-dc-dre-observacao="${linha.id}" title="${pidDcEscapeHtml(linha.observacao || "Adicionar observação")}">
${linha.observacao ? pidDcEscapeHtml(linha.observacao) : "+ Observação"}
</button>
</td>
`;
body.appendChild(tr);
});
}
document.getElementById("dc-dre-body").addEventListener("click", (event) => {
const toggleBtn = event.target.closest("[data-dc-dre-toggle]");
if (toggleBtn) {
const id = Number(toggleBtn.getAttribute("data-dc-dre-toggle"));
const estavaColapsada = dcDreColapsadas.has(id);
if (estavaColapsada) dcDreColapsadas.delete(id);
else dcDreColapsadas.add(id);
const nivelFn = (linha) => Math.max(0, linha.nivel);
dcDreDestaque = estavaColapsada ? dcFilhosDiretos(apuracaoAtual.linhas_dre, nivelFn, id) : new Set();
renderDre();
return;
}
const btn = event.target.closest("[data-dc-dre-observacao]");
if (!btn) return;
abrirModalObservacao("dre", Number(btn.getAttribute("data-dc-dre-observacao")));
});
// ---- Tabs ----
document.getElementById("dc-tabs").addEventListener("click", async (event) => {
const tabBtn = event.target.closest("[data-dc-tab]");
if (!tabBtn) return;
const tab = tabBtn.getAttribute("data-dc-tab");
document.querySelectorAll("#dc-tabs .pa-tab").forEach((t) => t.classList.toggle("is-active", t === tabBtn));
document.querySelectorAll("[data-dc-panel]").forEach((panel) => {
panel.hidden = panel.getAttribute("data-dc-panel") !== tab;
});
if (tab === "dashboard") await renderDashboardTab();
});
// ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ----
// Indicadores só são buscados na primeira vez que a aba é aberta (mesmo
// espírito de "canvas com tamanho zero não desenha certo" do relatório em
// si — aqui o motivo é só evitar um cálculo/consulta extra ao banco em
// toda apuração aberta, já que boa parte das revisões nem chega a olhar
// esta aba). Observações (contas/DRE/achados) já estão todas carregadas
// em apuracaoAtual, não precisam de fetch próprio.
async function renderDashboardTab() {
if (!dcIndicadoresAtual) {
dcIndicadoresAtual = await pidBuscarIndicadoresContabil(apuracaoAtual.id);
}
renderDashboardIndicadores();
renderDashboardObservacoes();
}
// Agrupa as chaves de `metadados` pelo campo `grupo` de cada uma (vem do
// servidor — os 11 de sistema já têm um grupo fixo, personalizados
// sempre caem em "Indicadores Personalizados", ver
// `ContabilApuracaoViewSet.indicadores()`); só a ORDEM de exibição dos
// grupos é decidida aqui (`PID_DC_DASH_ORDEM_GRUPOS`).
function renderDashboardIndicadores() {
const container = document.getElementById("dc-dash-indicadores");
container.innerHTML = "";
if (!dcIndicadoresAtual) return;
const { indicadores, indicadores_ocultos: indicadoresOcultos, metadados } = dcIndicadoresAtual;
const concluida = apuracaoAtual.status === "concluida";
const grupos = new Map();
Object.keys(metadados).forEach((chave) => {
const nomeGrupo = metadados[chave].grupo;
if (!grupos.has(nomeGrupo)) grupos.set(nomeGrupo, []);
grupos.get(nomeGrupo).push(chave);
});
const nomesGrupos = [...grupos.keys()].sort((a, b) => {
const posicaoA = PID_DC_DASH_ORDEM_GRUPOS.indexOf(a);
const posicaoB = PID_DC_DASH_ORDEM_GRUPOS.indexOf(b);
return (posicaoA === -1 ? 99 : posicaoA) - (posicaoB === -1 ? 99 : posicaoB);
});
nomesGrupos.forEach((nomeGrupo) => {
const secao = document.createElement("div");
secao.className = "dc-dash-grupo";
secao.innerHTML = `
<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" ----
const indicadorModal = document.getElementById("dc-indicador-modal");
const indicadorComponentesEl = document.getElementById("dc-indicador-componentes");
let dcIndicadorEmEdicao = null; // definição completa (id/chave/.../componentes) ou null = criando
let dcIndicadorComponenteSeq = 0;
const PID_DC_COMP_TIPO_LABELS = {
contas: "Soma de contas do Balancete",
linha_dre: "Soma de linhas da DRE",
variacao_conta: "Variação de contas (atual − anterior)",
indicador: "Referência a outro indicador",
resultado_liquido: "Resultado Líquido da DRE (última linha)",
};
function pidDcRenderComponentePicker(linhaEl, tipo, valoresIniciais) {
const picker = linhaEl.querySelector(".dc-ind-comp-picker");
if (tipo === "contas" || tipo === "variacao_conta") {
const selecionados = valoresIniciais.contas_codigos || [];
const codigosNestaApuracao = new Set(apuracaoAtual.contas.map((conta) => conta.codigo));
// Um indicador pode ter sido criado a partir de outra apuração
// (empresa/competência diferente) — código selecionado que não
// existe aqui ainda precisa continuar marcado e ser salvo de volta
// do jeito que estava, senão editar um indicador desses nesta tela
// apagaria silenciosamente a conta que só existe na apuração
// original. Aparece como item extra, sinalizado, no topo da lista.
const orfaos = selecionados.filter((codigo) => !codigosNestaApuracao.has(codigo));
const selecionadosSet = new Set(selecionados);
picker.innerHTML = `
<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-padrao").checked = definicao ? definicao.padrao : true;
document.getElementById("dc-indicador-modal-error").textContent = "";
document.getElementById("dc-indicador-modal-excluir-btn").hidden = !definicao;
document.getElementById("dc-indicador-preview").hidden = true;
indicadorComponentesEl.innerHTML = "";
const componentesIniciais = definicao ? definicao.componentes : [];
if (componentesIniciais.length) {
componentesIniciais.forEach((componente) => indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente(componente)));
} else {
indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente({}));
}
indicadorModal.hidden = false;
}
function pidDcFecharIndicadorModal() {
indicadorModal.hidden = true;
dcIndicadorEmEdicao = null;
}
document.getElementById("dc-indicador-modal-cancel-btn").addEventListener("click", pidDcFecharIndicadorModal);
indicadorModal.addEventListener("click", (event) => {
if (event.target === indicadorModal) pidDcFecharIndicadorModal();
});
// Depois de criar/editar/excluir, além de recarregar os cards da aba
// "Dashboard", atualiza o hub também se ele estiver aberto por trás
// (o form de indicador é sempre stacked por cima dele, ver
// `.modal-overlay--top`/pidDcAbrirHubIndicadores) — sem isso, o hub
// ficaria com a lista desatualizada até ser reaberto.
async function pidDcAposSalvarOuExcluirIndicador() {
dcIndicadoresAtual = null;
await renderDashboardTab();
if (!indicadoresHubModal.hidden) await pidDcRenderHubIndicadores();
}
// Reaproveitada pelo "Salvar" e pelo "Calcular com esta apuração" — os
// dois precisam exatamente da mesma leitura do formulário (chave/tipo/
// seleção de cada linha de componente).
function pidDcColetaComponentesForm() {
return [...indicadorComponentesEl.querySelectorAll(".dc-ind-componente")].map((linha) => {
const tipo = linha.querySelector(".dc-ind-comp-tipo").value;
const chave = linha.querySelector(".dc-ind-comp-chave").value.trim();
const componente = { chave, tipo, contas_codigos: [], linhas_dre_descricoes: [], indicador_referenciado: "" };
if (tipo === "contas" || tipo === "variacao_conta") {
componente.contas_codigos = [...linha.querySelectorAll('[data-dc-comp-lista] input[type="checkbox"]:checked')].map(
(cb) => cb.value
);
} else if (tipo === "linha_dre") {
componente.linhas_dre_descricoes = [
...linha.querySelectorAll('[data-dc-comp-lista] input[type="checkbox"]:checked'),
].map((cb) => cb.value);
} else if (tipo === "indicador") {
const select = linha.querySelector(".dc-ind-comp-indicador");
componente.indicador_referenciado = select ? select.value : "";
}
return componente;
});
}
document.getElementById("dc-indicador-modal-save-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("dc-indicador-modal-error");
errorEl.textContent = "";
const componentes = pidDcColetaComponentesForm();
const payload = {
nome: document.getElementById("dc-indicador-modal-nome").value.trim(),
descricao: document.getElementById("dc-indicador-modal-descricao").value.trim(),
formato: document.getElementById("dc-indicador-modal-formato").value,
formula: document.getElementById("dc-indicador-modal-formula").value.trim(),
padrao: document.getElementById("dc-indicador-modal-padrao").checked,
componentes,
};
if (!payload.nome) {
errorEl.textContent = "Informe o nome do indicador.";
return;
}
if (!componentes.length) {
errorEl.textContent = "Adicione pelo menos um componente.";
return;
}
if (componentes.some((c) => !c.chave)) {
errorEl.textContent = "Preencha a chave de todos os componentes.";
return;
}
if (!payload.formula) {
errorEl.textContent = "Informe a fórmula do indicador.";
return;
}
const saveBtn = document.getElementById("dc-indicador-modal-save-btn");
saveBtn.disabled = true;
try {
if (dcIndicadorEmEdicao) {
await pidAtualizarIndicadorContabil(dcIndicadorEmEdicao.id, payload);
} else {
await pidCriarIndicadorContabil(payload);
}
pidDcFecharIndicadorModal();
await pidDcAposSalvarOuExcluirIndicador();
} catch (e) {
errorEl.textContent = e.message;
} finally {
saveBtn.disabled = false;
}
});
// Calcula, contra a apuração aberta agora, o valor de cada componente e
// o resultado final — sem salvar nada — pro contador conferir se a
// fórmula pegou a conta/linha certa antes de confirmar. Reaproveita a
// mesma coleta do formulário do botão "Salvar" (`pidDcColetaComponentesForm`).
document.getElementById("dc-indicador-calcular-btn").addEventListener("click", async () => {
const previewEl = document.getElementById("dc-indicador-preview");
const componentes = pidDcColetaComponentesForm();
const formula = document.getElementById("dc-indicador-modal-formula").value.trim();
previewEl.hidden = false;
if (!componentes.length || componentes.some((c) => !c.chave) || !formula) {
previewEl.innerHTML = `<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);
}
});
// ---- Modal de observação (reaproveitado por conta do Balancete e linha da DRE) ----
const observacaoModal = document.getElementById("dc-observacao-modal");
let observacaoEmEdicaoTipo = null; // "conta" | "dre"
let observacaoEmEdicaoId = null;
function abrirModalObservacao(tipo, id) {
const lista = tipo === "dre" ? apuracaoAtual.linhas_dre : apuracaoAtual.contas;
const item = lista.find((i) => i.id === id);
if (!item) return;
observacaoEmEdicaoTipo = tipo;
observacaoEmEdicaoId = id;
document.getElementById("dc-observacao-modal-conta").textContent =
tipo === "dre" ? item.descricao : `${item.codigo} — ${item.descricao}`;
document.getElementById("dc-observacao-modal-texto").value = item.observacao || "";
document.getElementById("dc-observacao-modal-error").textContent = "";
document.getElementById("dc-observacao-modal-texto").disabled = apuracaoAtual.status === "concluida";
document.getElementById("dc-observacao-modal-save-btn").hidden = apuracaoAtual.status === "concluida";
observacaoModal.hidden = false;
}
function fecharModalObservacao() {
observacaoModal.hidden = true;
observacaoEmEdicaoTipo = null;
observacaoEmEdicaoId = null;
}
document.getElementById("dc-observacao-modal-cancel-btn").addEventListener("click", fecharModalObservacao);
observacaoModal.addEventListener("click", (event) => {
if (event.target === observacaoModal) fecharModalObservacao();
});
document.getElementById("dc-observacao-modal-save-btn").addEventListener("click", async () => {
const errorEl = document.getElementById("dc-observacao-modal-error");
errorEl.textContent = "";
const texto = document.getElementById("dc-observacao-modal-texto").value;
try {
if (observacaoEmEdicaoTipo === "dre") {
const linha = await pidAtualizarObservacaoLinhaDreContabil(observacaoEmEdicaoId, texto);
const indice = apuracaoAtual.linhas_dre.findIndex((l) => l.id === observacaoEmEdicaoId);
if (indice >= 0) apuracaoAtual.linhas_dre[indice] = linha;
renderDre();
} else {
const conta = await pidAtualizarObservacaoContaContabil(observacaoEmEdicaoId, texto);
const indice = apuracaoAtual.contas.findIndex((c) => c.id === observacaoEmEdicaoId);
if (indice >= 0) apuracaoAtual.contas[indice] = conta;
renderContas();
}
fecharModalObservacao();
} catch (e) {
errorEl.textContent = 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");
});