const PID_DC_STATUS_LABELS = { revisao: "Em revisão", concluida: "Concluída" }; const PID_DC_SEVERIDADE_LABELS = { alta: "Alta", media: "Média", baixa: "Baixa" }; const PID_DC_ACHADO_STATUS_LABELS = { pendente: "Pendente", tratado: "Tratado", ignorado: "Ignorado" }; // Ordem de exibição do donut/legenda de severidade no resumo — mesmas cores // já usadas pelos badges (.dc-badge--alta/--media/--baixa, dashboard-contabil.css). const PID_DC_SEVERIDADE_LISTA = ["alta", "media", "baixa"]; const PID_DC_SEVERIDADE_CORES = { alta: "var(--danger)", media: "var(--gold)", baixa: "rgb(var(--slate-rgb))" }; // Mesmas 10 regras/chaves de regras.py (REGRAS) — enumeradas aqui pra sempre // mostrar as 10 categorias no resumo, mesmo as que não geraram achado nesta // apuração (mesmo espírito do "Nenhum registro encontrado" do sistema externo // que inspirou esta visão). const PID_DC_REGRAS = [ { chave: "balanceamento_ativo_passivo", label: "Balanceamento Ativo x Passivo" }, { chave: "debito_credito_divergente", label: "Débito x Crédito" }, { chave: "saldo_negativo_caixa", label: "Caixa Negativo" }, { chave: "conta_transitoria_com_saldo", label: "Contas Transitórias" }, { chave: "conta_deveria_zerar", label: "Contas que Deveriam Zerar" }, { chave: "saldo_sinal_invertido", label: "Sinal de Saldo Invertido" }, { chave: "descricao_generica", label: "Descrição Genérica" }, { chave: "variacao_atipica_saldo", label: "Variação Atípica de Saldo" }, { chave: "variacao_atipica_dre", label: "Variação Atípica na DRE" }, { chave: "percentual_custo_receita_atipico", label: "Percentual Custo/Receita" }, ]; // Agrupamento temático das 10 regras acima pro resumo da aba Observações — // evita 10 cards soltos no grid (poluído visualmente com uma tabela por // conta em cada um); cada grupo vira 1 card com as regras dele listadas por // linha (label + contagem), clique numa linha com contagem > 0 continua // isolando achado.regra na lista abaixo, igual antes. const PID_DC_GRUPOS = [ { label: "Divergências de Saldo", regras: [ "balanceamento_ativo_passivo", "debito_credito_divergente", "saldo_negativo_caixa", "saldo_sinal_invertido", ], }, { label: "Contas Atípicas", regras: ["conta_transitoria_com_saldo", "conta_deveria_zerar", "descricao_generica"], }, { label: "Variações e Indicadores", regras: ["variacao_atipica_saldo", "variacao_atipica_dre", "percentual_custo_receita_atipico"], }, ]; // Paleta de 10 cores distintas reaproveitando os tokens *-rgb já existentes // (theme-aware — acompanham o tema claro/escuro e a cor de tema escolhida // pelo usuário), com color-mix() pra completar até 10 sem hardcoded hex novo. const PID_DC_CATEGORIA_CORES = [ "rgb(var(--accent-rgb))", "rgb(var(--danger-rgb))", "rgb(var(--gold-rgb))", "rgb(var(--teal-rgb))", "rgb(var(--slate-rgb))", "rgb(var(--coral-rgb))", "color-mix(in srgb, rgb(var(--accent-rgb)) 55%, white)", "color-mix(in srgb, rgb(var(--danger-rgb)) 55%, white)", "color-mix(in srgb, rgb(var(--gold-rgb)) 55%, black)", "color-mix(in srgb, rgb(var(--teal-rgb)) 55%, black)", ]; function pidDcFormatMoeda(valor) { const numero = Number(valor || 0); return numero.toLocaleString("pt-BR", { style: "currency", currency: "BRL" }); } // Os 3 formatos abaixo espelham os filtros moeda/percentual/indice de // portal_api/templatetags/contabil_extras.py (mesmo relatório "Gerar // Dashboard") — diferente de pidDcFormatMoeda acima, `null`/`undefined` // sempre vira "—", nunca "R$ 0,00": em IndicadoresFinanceiros, `None` // significa "indisponível" (ex. EBITDA sem histórico), não zero. function pidDcFormatIndicadorMoeda(valor) { if (valor === null || valor === undefined) return "—"; return Number(valor).toLocaleString("pt-BR", { style: "currency", currency: "BRL" }); } function pidDcFormatIndicadorPercentual(valor) { if (valor === null || valor === undefined) return "—"; return `${(Number(valor) * 100).toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%`; } function pidDcFormatIndicadorIndice(valor) { if (valor === null || valor === undefined) return "—"; return Number(valor).toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } // Label/grupo/formato de cada indicador (de sistema ou personalizado) vêm // sempre do servidor agora (`GET /contabil-apuracoes/{id}/indicadores/`, // campo `metadados`) — única fonte de verdade, já que um indicador // personalizado não tem como ter seu formato hardcoded aqui. Só a ORDEM de // exibição dos grupos continua fixa no frontend (grupo desconhecido, se // algum dia existir, cai no fim). const PID_DC_DASH_ORDEM_GRUPOS = [ "Indicadores de Resultado", "Indicadores de Liquidez e Endividamento", "Indicadores Personalizados", ]; function pidDcFormatIndicadorGenerico(formato, valor) { if (formato === "moeda") return pidDcFormatIndicadorMoeda(valor); if (formato === "percentual") return pidDcFormatIndicadorPercentual(valor); return pidDcFormatIndicadorIndice(valor); } const PID_DC_ICON_OLHO_ABERTO = ''; const PID_DC_ICON_OLHO_FECHADO = ''; const PID_DC_ICON_EDITAR = ''; const PID_DC_ICON_LIXEIRA = ''; function pidDcFormatCompetencia(iso) { if (!iso) return ""; const [ano, mes] = iso.split("-"); return `${mes}/${ano}`; } function pidDcFormatData(iso) { if (!iso) return ""; return new Date(iso).toLocaleDateString("pt-BR"); } function pidDcEscapeHtml(texto) { const div = document.createElement("div"); div.textContent = texto == null ? "" : String(texto); return div.innerHTML; } async function pidFetchApuracoesContabil() { return pidApiRequest("/contabil-apuracoes/"); } async function pidFetchApuracaoContabil(id) { return pidApiRequest(`/contabil-apuracoes/${id}/`); } async function pidCriarApuracaoContabil(formData) { return pidApiRequest("/contabil-apuracoes/", { method: "POST", body: formData }); } async function pidExcluirApuracaoContabil(id) { return pidApiRequest(`/contabil-apuracoes/${id}/`, { method: "DELETE" }); } async function pidConcluirApuracaoContabil(id) { return pidApiRequest(`/contabil-apuracoes/${id}/concluir/`, { method: "POST" }); } /** * /dashboard/ é GET (não POST, diferente do padrão /gerar/ de outras * ferramentas) exatamente pra poder ser aberto assim — navegação de * verdade numa nova aba, não um blob em memória. Um blob criado via fetch * (`URL.createObjectURL`) tem sua própria origem sintética: URLs relativas * dentro do HTML (como `{% static %}` da logo do escritório, que gera * "/static/...") não resolvem de forma confiável contra a origem do Portal * dentro de um documento carregado de um `blob:`, quebrando a imagem — com * navegação de verdade pra URL da API, isso deixa de ser um problema. */ function pidGerarDashboardContabil(id) { window.open(`/api/contabil-apuracoes/${id}/dashboard/`, "_blank"); } async function pidAtualizarObservacaoContaContabil(id, observacao) { return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: { observacao } }); } async function pidAtualizarObservacaoLinhaDreContabil(id, observacao) { return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: { observacao } }); } async function pidAtualizarAchadoContabil(id, status, observacaoContador) { return pidApiRequest(`/contabil-achados/${id}/`, { method: "PATCH", body: { status, observacao_contador: observacaoContador }, }); } // ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ---- async function pidBuscarIndicadoresContabil(id) { return pidApiRequest(`/contabil-apuracoes/${id}/indicadores/`); } async function pidAtualizarIndicadoresOcultosContabil(id, indicadoresOcultos) { return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-ocultos/`, { method: "POST", body: { indicadores_ocultos: indicadoresOcultos }, }); } async function pidAlternarOcultoContaContabil(id, oculto) { return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: { oculta_no_relatorio: oculto } }); } async function pidAlternarOcultoLinhaDreContabil(id, oculto) { return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: { oculta_no_relatorio: oculto } }); } async function pidAlternarOcultoAchadoContabil(id, oculto) { return pidApiRequest(`/contabil-achados/${id}/alternar-oculto/`, { method: "POST", body: { oculto_no_relatorio: oculto }, }); } async function pidBuscarIndicadorContabilDefinicao(id) { return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`); } async function pidCriarIndicadorContabil(payload) { return pidApiRequest("/contabil-indicadores-definicoes/", { method: "POST", body: payload }); } async function pidAtualizarIndicadorContabil(id, payload) { return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`, { method: "PATCH", body: payload }); } async function pidExcluirIndicadorContabil(id) { return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`, { method: "DELETE" }); } async function pidBuscarDefinicoesIndicadoresContabil() { return pidApiRequest("/contabil-indicadores-definicoes/"); } async function pidPreVisualizarIndicadorContabil(apuracaoId, payload) { return pidApiRequest(`/contabil-apuracoes/${apuracaoId}/pre-visualizar-indicador/`, { method: "POST", body: payload, }); } async function pidAtualizarIndicadoresSelecionadosContabil(id, indicadoresSelecionados) { return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-selecionados/`, { method: "POST", body: { indicadores_selecionados: indicadoresSelecionados }, }); } document.addEventListener("DOMContentLoaded", async () => { const listView = document.getElementById("dc-list-view"); if (!listView) return; const me = await pidGetMe(); if (!me) return; const relatoriosPerms = (me.permissoes_efetivas && me.permissoes_efetivas["relatorios"]) || {}; const canUse = !!(relatoriosPerms.enabled && relatoriosPerms.apps && relatoriosPerms.apps["dashboard-contabil"]); const noAccessEl = document.getElementById("dc-no-access"); const contentEl = document.getElementById("dc-content"); if (noAccessEl) noAccessEl.hidden = canUse; if (contentEl) contentEl.hidden = !canUse; if (!canUse) return; const formView = document.getElementById("dc-form-view"); const reviewView = document.getElementById("dc-review-view"); let apuracaoAtual = null; let filtroSeveridade = "todas"; let filtroStatusAchado = "pendente"; // Categoria (achado.regra) selecionada clicando num card do resumo — null // = nenhuma categoria isolada. Filtro independente de severidade/status, // os três se combinam (E lógico) na lista detalhada abaixo do resumo. let filtroRegra = null; let achadoEmEdicao = null; // Ids de ContabilConta com filhos recolhidos na árvore do Balancete — // reiniciado toda vez que uma apuração é aberta/criada (ver renderRevisao). let dcContasColapsadas = new Set(); // Mesma ideia, mas para a árvore da DRE (ContabilLinhaDre.id) — nível de // indentação já vem pronto do backend (linha.nivel), sem precisar derivar // de um código de classificação como no Balancete. let dcDreColapsadas = new Set(); // Ids de ContabilAchado com o detalhe da conta usada no apontamento // expandido (ver renderAchados) — só achados com `conta` preenchido (as // regras gerais, ex. balanceamento Ativo x Passivo, não têm uma conta // única por trás, então não ganham o toggle). let dcAchadosContaExpandida = new Set(); // { indicadores, indicadores_ocultos } da aba "Dashboard" — carregado sob // demanda (só quando essa aba é aberta pela primeira vez após abrir/criar // a apuração, ver renderDashboardTab), null até lá. let dcIndicadoresAtual = null; function showView(view) { listView.hidden = view !== "list"; formView.hidden = view !== "form"; reviewView.hidden = view !== "review"; } // ---- Histórico ---- async function carregarLista() { const apuracoes = await pidFetchApuracoesContabil(); const body = document.getElementById("dc-list-body"); const empty = document.getElementById("dc-list-empty"); body.innerHTML = ""; empty.hidden = apuracoes.length > 0; apuracoes.forEach((apuracao) => { const tr = document.createElement("tr"); tr.innerHTML = ` ${pidDcEscapeHtml(apuracao.nome_empresa)} ${pidDcFormatCompetencia(apuracao.competencia)} ${apuracao.total_achados_pendentes} ${PID_DC_STATUS_LABELS[apuracao.status] || apuracao.status} ${pidDcEscapeHtml(apuracao.criado_por_nome || "-")} ${pidDcFormatData(apuracao.criado_em)} `; 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 = ``; 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 += ` `; }); } 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 = `${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 = `
${pidDcEscapeHtml(grupo.label)} ${totalGrupo}
`; 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 = ` ${pidDcEscapeHtml(regraInfo.label)} ${itens.length} `; 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 = `
${PID_DC_SEVERIDADE_LABELS[achado.severidade] || achado.severidade} ${PID_DC_ACHADO_STATUS_LABELS[achado.status] || achado.status} ${pidDcEscapeHtml(achado.titulo)}
${pidDcEscapeHtml(achado.mensagem)}
${conta ? ` ${expandida ? `
Conta
${pidDcEscapeHtml(conta.codigo)} — ${pidDcEscapeHtml(conta.descricao)}
Saldo Anterior
${pidDcFormatMoeda(conta.saldo_anterior)}
Débito
${pidDcFormatMoeda(conta.debito)}
Crédito
${pidDcFormatMoeda(conta.credito)}
Saldo Atual
${pidDcFormatMoeda(conta.saldo_atual)}
` : ""} ` : ""} ${achado.observacao_contador ? `
${pidDcEscapeHtml(achado.observacao_contador)}${achado.tratado_por_nome ? ` — ${pidDcEscapeHtml(achado.tratado_por_nome)}` : ""}
` : ""}
${podeEditar ? `` : ""}
`; 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] ? `` : ``; 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 = ` ${pidDcEscapeHtml(conta.codigo)} ${toggle}${pidDcEscapeHtml(conta.descricao)} ${pidDcFormatMoeda(conta.saldo_anterior)} ${pidDcFormatMoeda(conta.debito)} ${pidDcFormatMoeda(conta.credito)} ${pidDcFormatMoeda(conta.saldo_atual)} `; body.appendChild(tr); }); } document.getElementById("dc-contas-body").addEventListener("click", (event) => { const toggleBtn = event.target.closest("[data-dc-conta-toggle]"); if (toggleBtn) { const id = Number(toggleBtn.getAttribute("data-dc-conta-toggle")); 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] ? `` : ``; 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 = ` ${toggle}${pidDcEscapeHtml(linha.descricao)} ${pidDcFormatMoeda(linha.valor)} `; body.appendChild(tr); }); } document.getElementById("dc-dre-body").addEventListener("click", (event) => { const toggleBtn = event.target.closest("[data-dc-dre-toggle]"); if (toggleBtn) { const id = Number(toggleBtn.getAttribute("data-dc-dre-toggle")); 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 = `

${pidDcEscapeHtml(nomeGrupo)}

`; const grid = secao.querySelector(".dc-dash-cards-grid"); grupos.get(nomeGrupo).forEach((chave) => { const meta = metadados[chave]; const oculto = indicadoresOcultos.includes(chave); const card = document.createElement("div"); card.className = `dc-dash-card${oculto ? " is-oculto" : ""}`; card.innerHTML = `
${pidDcEscapeHtml(meta.nome)} ${pidDcFormatIndicadorGenerico(meta.formato, indicadores[chave])}
`; card.addEventListener("click", (event) => { if (event.target.closest("button")) return; pidDcAbrirFormulaModal(meta); }); grid.appendChild(card); }); container.appendChild(secao); }); } document.getElementById("dc-dash-indicadores").addEventListener("click", async (event) => { const editarBtn = event.target.closest("[data-dc-dash-indicador-editar]"); if (editarBtn) { if (editarBtn.disabled) return; const definicaoId = Number(editarBtn.getAttribute("data-dc-dash-indicador-editar")); try { const definicao = await pidBuscarIndicadorContabilDefinicao(definicaoId); pidDcAbrirIndicadorModal(definicao); } catch (e) { await pidAlert(e.message); } return; } const btn = event.target.closest("[data-dc-dash-indicador-toggle]"); if (!btn || btn.disabled) return; const chave = btn.getAttribute("data-dc-dash-indicador-toggle"); const atual = dcIndicadoresAtual.indicadores_ocultos; const novaLista = atual.includes(chave) ? atual.filter((c) => c !== chave) : [...atual, chave]; btn.disabled = true; try { const resposta = await pidAtualizarIndicadoresOcultosContabil(apuracaoAtual.id, novaLista); dcIndicadoresAtual.indicadores_ocultos = resposta.indicadores_ocultos; renderDashboardIndicadores(); } catch (e) { btn.disabled = false; await pidAlert(e.message); } }); // ---- Modal "Ver fórmula" ---- // Aceita um objeto {nome, descricao, formula_texto} direto — não só uma // chave de `dcIndicadoresAtual.metadados` (que só lista indicadores // ATIVOS nesta apuração). Um indicador não padrão ainda não selecionado // pra esta apuração não aparece em `metadados`, mas o hub de // "Gerenciar Indicadores" (ver pidDcCriaLinhaHub) ainda precisa poder // mostrar a fórmula dele — por isso o chamador monta o objeto certo em // cada caso, em vez desta função sempre resolver por chave. function pidDcAbrirFormulaModal(meta) { if (!meta) return; document.getElementById("dc-indicador-formula-modal-titulo").textContent = meta.nome; document.getElementById("dc-indicador-formula-modal-descricao").textContent = meta.descricao || "Sem descrição cadastrada."; document.getElementById("dc-indicador-formula-modal-formula").textContent = meta.formula_texto || "—"; document.getElementById("dc-indicador-formula-modal").hidden = false; } document.getElementById("dc-indicador-formula-modal-fechar-btn").addEventListener("click", () => { document.getElementById("dc-indicador-formula-modal").hidden = true; }); document.getElementById("dc-indicador-formula-modal").addEventListener("click", (event) => { if (event.target.id === "dc-indicador-formula-modal") { document.getElementById("dc-indicador-formula-modal").hidden = true; } }); // ---- Modal "Novo/Editar Indicador" ---- const indicadorModal = document.getElementById("dc-indicador-modal"); const indicadorComponentesEl = document.getElementById("dc-indicador-componentes"); let dcIndicadorEmEdicao = null; // definição completa (id/chave/.../componentes) ou null = criando let dcIndicadorComponenteSeq = 0; const PID_DC_COMP_TIPO_LABELS = { contas: "Soma de contas do Balancete", linha_dre: "Soma de linhas da DRE", variacao_conta: "Variação de contas (atual − anterior)", indicador: "Referência a outro indicador", resultado_liquido: "Resultado Líquido da DRE (última linha)", }; function pidDcRenderComponentePicker(linhaEl, tipo, valoresIniciais) { const picker = linhaEl.querySelector(".dc-ind-comp-picker"); if (tipo === "contas" || tipo === "variacao_conta") { const selecionados = valoresIniciais.contas_codigos || []; const codigosNestaApuracao = new Set(apuracaoAtual.contas.map((conta) => conta.codigo)); // Um indicador pode ter sido criado a partir de outra apuração // (empresa/competência diferente) — código selecionado que não // existe aqui ainda precisa continuar marcado e ser salvo de volta // do jeito que estava, senão editar um indicador desses nesta tela // apagaria silenciosamente a conta que só existe na apuração // original. Aparece como item extra, sinalizado, no topo da lista. const orfaos = selecionados.filter((codigo) => !codigosNestaApuracao.has(codigo)); const selecionadosSet = new Set(selecionados); picker.innerHTML = `
${orfaos.map((codigo) => ` `).join("")} ${apuracaoAtual.contas.map((conta) => ` `).join("")}
`; } else if (tipo === "linha_dre") { const selecionados = valoresIniciais.linhas_dre_descricoes || []; const descricoesNestaApuracao = new Set(apuracaoAtual.linhas_dre.map((linha) => linha.descricao)); const orfaos = selecionados.filter((descricao) => !descricoesNestaApuracao.has(descricao)); const selecionadosSet = new Set(selecionados); picker.innerHTML = `
${orfaos.map((descricao) => ` `).join("")} ${apuracaoAtual.linhas_dre.map((linha) => ` `).join("")}
`; } else if (tipo === "indicador") { const atual = valoresIniciais.indicador_referenciado || ""; const chaveEditando = dcIndicadorEmEdicao ? dcIndicadorEmEdicao.chave : null; const metadados = (dcIndicadoresAtual && dcIndicadoresAtual.metadados) || {}; const opcoes = Object.keys(metadados) .filter((chave) => chave !== chaveEditando) .sort((a, b) => metadados[a].nome.localeCompare(metadados[b].nome)); picker.innerHTML = ` `; } else if (tipo === "resultado_liquido") { // Sem seleção nenhuma — sempre resolve pra última linha da DRE desta // apuração, seja qual for o rótulo (evita quebrar quando o resultado // muda de sinal e a linha passa de "LUCRO" pra "PREJUÍZO", ou // vice-versa, ver dashboard_contabil/models.py). picker.innerHTML = `

Usa automaticamente o resultado líquido do período (última linha da DRE desta apuração), sem precisar escolher nada aqui.

`; } else { picker.innerHTML = ""; } } function pidDcCriaLinhaComponente(valoresIniciais) { dcIndicadorComponenteSeq += 1; const linha = document.createElement("div"); linha.className = "dc-ind-componente"; linha.dataset.dcCompId = String(dcIndicadorComponenteSeq); const tipoInicial = valoresIniciais.tipo || "contas"; linha.innerHTML = `
`; pidDcRenderComponentePicker(linha, tipoInicial, valoresIniciais); return linha; } indicadorComponentesEl.addEventListener("change", (event) => { if (event.target.classList.contains("dc-ind-comp-tipo")) { const linha = event.target.closest(".dc-ind-componente"); pidDcRenderComponentePicker(linha, event.target.value, {}); } }); indicadorComponentesEl.addEventListener("input", (event) => { if (!event.target.hasAttribute("data-dc-comp-busca")) return; const linha = event.target.closest(".dc-ind-componente"); const termo = event.target.value.trim().toLowerCase(); linha.querySelectorAll("[data-dc-comp-item]").forEach((item) => { item.hidden = termo !== "" && !item.getAttribute("data-dc-comp-item").includes(termo); }); }); indicadorComponentesEl.addEventListener("click", (event) => { const removerBtn = event.target.closest(".dc-ind-comp-remover"); if (removerBtn) removerBtn.closest(".dc-ind-componente").remove(); }); document.getElementById("dc-indicador-add-componente-btn").addEventListener("click", () => { indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente({})); }); function pidDcAbrirIndicadorModal(definicao) { dcIndicadorEmEdicao = definicao || null; document.getElementById("dc-indicador-modal-titulo").textContent = definicao ? "Editar Indicador" : "Novo Indicador"; document.getElementById("dc-indicador-modal-nome").value = definicao ? definicao.nome : ""; document.getElementById("dc-indicador-modal-descricao").value = definicao ? definicao.descricao : ""; document.getElementById("dc-indicador-modal-formato").value = definicao ? definicao.formato : "indice"; document.getElementById("dc-indicador-modal-formula").value = definicao ? definicao.formula : ""; document.getElementById("dc-indicador-modal-padrao").checked = definicao ? definicao.padrao : true; document.getElementById("dc-indicador-modal-error").textContent = ""; document.getElementById("dc-indicador-modal-excluir-btn").hidden = !definicao; document.getElementById("dc-indicador-preview").hidden = true; indicadorComponentesEl.innerHTML = ""; const componentesIniciais = definicao ? definicao.componentes : []; if (componentesIniciais.length) { componentesIniciais.forEach((componente) => indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente(componente))); } else { indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente({})); } indicadorModal.hidden = false; } function pidDcFecharIndicadorModal() { indicadorModal.hidden = true; dcIndicadorEmEdicao = null; } document.getElementById("dc-indicador-modal-cancel-btn").addEventListener("click", pidDcFecharIndicadorModal); indicadorModal.addEventListener("click", (event) => { if (event.target === indicadorModal) pidDcFecharIndicadorModal(); }); // Depois de criar/editar/excluir, além de recarregar os cards da aba // "Dashboard", atualiza o hub também se ele estiver aberto por trás // (o form de indicador é sempre stacked por cima dele, ver // `.modal-overlay--top`/pidDcAbrirHubIndicadores) — sem isso, o hub // ficaria com a lista desatualizada até ser reaberto. async function pidDcAposSalvarOuExcluirIndicador() { dcIndicadoresAtual = null; await renderDashboardTab(); if (!indicadoresHubModal.hidden) await pidDcRenderHubIndicadores(); } // Reaproveitada pelo "Salvar" e pelo "Calcular com esta apuração" — os // dois precisam exatamente da mesma leitura do formulário (chave/tipo/ // seleção de cada linha de componente). function pidDcColetaComponentesForm() { return [...indicadorComponentesEl.querySelectorAll(".dc-ind-componente")].map((linha) => { const tipo = linha.querySelector(".dc-ind-comp-tipo").value; const chave = linha.querySelector(".dc-ind-comp-chave").value.trim(); const componente = { chave, tipo, contas_codigos: [], linhas_dre_descricoes: [], indicador_referenciado: "" }; if (tipo === "contas" || tipo === "variacao_conta") { componente.contas_codigos = [...linha.querySelectorAll('[data-dc-comp-lista] input[type="checkbox"]:checked')].map( (cb) => cb.value ); } else if (tipo === "linha_dre") { componente.linhas_dre_descricoes = [ ...linha.querySelectorAll('[data-dc-comp-lista] input[type="checkbox"]:checked'), ].map((cb) => cb.value); } else if (tipo === "indicador") { const select = linha.querySelector(".dc-ind-comp-indicador"); componente.indicador_referenciado = select ? select.value : ""; } return componente; }); } document.getElementById("dc-indicador-modal-save-btn").addEventListener("click", async () => { const errorEl = document.getElementById("dc-indicador-modal-error"); errorEl.textContent = ""; const componentes = pidDcColetaComponentesForm(); const payload = { nome: document.getElementById("dc-indicador-modal-nome").value.trim(), descricao: document.getElementById("dc-indicador-modal-descricao").value.trim(), formato: document.getElementById("dc-indicador-modal-formato").value, formula: document.getElementById("dc-indicador-modal-formula").value.trim(), padrao: document.getElementById("dc-indicador-modal-padrao").checked, componentes, }; if (!payload.nome) { errorEl.textContent = "Informe o nome do indicador."; return; } if (!componentes.length) { errorEl.textContent = "Adicione pelo menos um componente."; return; } if (componentes.some((c) => !c.chave)) { errorEl.textContent = "Preencha a chave de todos os componentes."; return; } if (!payload.formula) { errorEl.textContent = "Informe a fórmula do indicador."; return; } const saveBtn = document.getElementById("dc-indicador-modal-save-btn"); saveBtn.disabled = true; try { if (dcIndicadorEmEdicao) { await pidAtualizarIndicadorContabil(dcIndicadorEmEdicao.id, payload); } else { await pidCriarIndicadorContabil(payload); } pidDcFecharIndicadorModal(); await pidDcAposSalvarOuExcluirIndicador(); } catch (e) { errorEl.textContent = e.message; } finally { saveBtn.disabled = false; } }); // Calcula, contra a apuração aberta agora, o valor de cada componente e // o resultado final — sem salvar nada — pro contador conferir se a // fórmula pegou a conta/linha certa antes de confirmar. Reaproveita a // mesma coleta do formulário do botão "Salvar" (`pidDcColetaComponentesForm`). document.getElementById("dc-indicador-calcular-btn").addEventListener("click", async () => { const previewEl = document.getElementById("dc-indicador-preview"); const componentes = pidDcColetaComponentesForm(); const formula = document.getElementById("dc-indicador-modal-formula").value.trim(); previewEl.hidden = false; if (!componentes.length || componentes.some((c) => !c.chave) || !formula) { previewEl.innerHTML = `

Preencha a chave de todos os componentes e a fórmula antes de calcular.

`; return; } const payload = { nome: document.getElementById("dc-indicador-modal-nome").value.trim() || "Pré-visualização", descricao: "", formato: document.getElementById("dc-indicador-modal-formato").value, formula, componentes, }; const calcularBtn = document.getElementById("dc-indicador-calcular-btn"); calcularBtn.disabled = true; previewEl.innerHTML = `

Calculando...

`; try { const resultado = await pidPreVisualizarIndicadorContabil(apuracaoAtual.id, payload); if (resultado.erro) { previewEl.innerHTML = `

${pidDcEscapeHtml(resultado.erro)}

`; } else { previewEl.innerHTML = `
Resultado: ${resultado.resultado_formatado === null ? "—" : pidDcEscapeHtml(resultado.resultado_formatado)}
`; } } catch (e) { previewEl.innerHTML = `

${pidDcEscapeHtml(e.message)}

`; } finally { calcularBtn.disabled = false; } }); document.getElementById("dc-indicador-modal-excluir-btn").addEventListener("click", async () => { if (!dcIndicadorEmEdicao) return; if ( !(await pidConfirm(`Excluir o indicador "${dcIndicadorEmEdicao.nome}"? Essa ação não pode ser desfeita.`, { perigoso: true, })) ) { return; } const errorEl = document.getElementById("dc-indicador-modal-error"); errorEl.textContent = ""; try { await pidExcluirIndicadorContabil(dcIndicadorEmEdicao.id); pidDcFecharIndicadorModal(); await pidDcAposSalvarOuExcluirIndicador(); } catch (e) { errorEl.textContent = e.message; } }); // ---- Modal "Gerenciar Indicadores" (hub: lista todos os indicadores — // padrão e não padrão — pra visualizar/editar/ativar, mesmo os que não // estão aparecendo nesta apuração agora) ---- const indicadoresHubModal = document.getElementById("dc-indicadores-hub-modal"); function pidDcCriaLinhaHub(definicao, ativo, concluida) { const linha = document.createElement("div"); linha.className = "dc-ind-hub-item"; linha.innerHTML = ` ${ ativo === null ? `` : `` } `; return linha; } async function pidDcRenderHubIndicadores() { const listaPadrao = document.getElementById("dc-ind-hub-lista-padrao"); const listaNaoPadrao = document.getElementById("dc-ind-hub-lista-nao-padrao"); const vazioPadrao = document.getElementById("dc-ind-hub-vazio-padrao"); const vazioNaoPadrao = document.getElementById("dc-ind-hub-vazio-nao-padrao"); listaPadrao.innerHTML = ""; listaNaoPadrao.innerHTML = ""; let definicoes; try { definicoes = await pidBuscarDefinicoesIndicadoresContabil(); } catch (e) { await pidAlert(e.message); return; } const selecionados = new Set((dcIndicadoresAtual && dcIndicadoresAtual.indicadores_selecionados) || []); const concluida = apuracaoAtual.status === "concluida"; const padrao = definicoes.filter((d) => d.padrao); const naoPadrao = definicoes.filter((d) => !d.padrao); vazioPadrao.hidden = padrao.length > 0; vazioNaoPadrao.hidden = naoPadrao.length > 0; padrao.forEach((definicao) => listaPadrao.appendChild(pidDcCriaLinhaHub(definicao, null, concluida))); naoPadrao.forEach((definicao) => listaNaoPadrao.appendChild(pidDcCriaLinhaHub(definicao, selecionados.has(definicao.chave), concluida)) ); } async function pidDcAbrirHubIndicadores() { indicadoresHubModal.hidden = false; await pidDcRenderHubIndicadores(); } document.getElementById("dc-indicadores-gerenciar-btn").addEventListener("click", pidDcAbrirHubIndicadores); document.getElementById("dc-indicadores-hub-fechar-btn").addEventListener("click", () => { indicadoresHubModal.hidden = true; }); indicadoresHubModal.addEventListener("click", (event) => { if (event.target === indicadoresHubModal) indicadoresHubModal.hidden = true; }); document.getElementById("dc-indicadores-hub-novo-btn").addEventListener("click", () => pidDcAbrirIndicadorModal(null)); indicadoresHubModal.addEventListener("click", async (event) => { const verBtn = event.target.closest("[data-dc-hub-ver]"); if (verBtn) { const definicaoId = Number(verBtn.getAttribute("data-dc-hub-ver")); try { const definicao = await pidBuscarIndicadorContabilDefinicao(definicaoId); pidDcAbrirFormulaModal({ nome: definicao.nome, descricao: definicao.descricao, formula_texto: definicao.formula, }); } catch (e) { await pidAlert(e.message); } return; } const editarBtn = event.target.closest("[data-dc-hub-editar]"); if (editarBtn) { if (editarBtn.disabled) return; const definicaoId = Number(editarBtn.getAttribute("data-dc-hub-editar")); try { const definicao = await pidBuscarIndicadorContabilDefinicao(definicaoId); pidDcAbrirIndicadorModal(definicao); } catch (e) { await pidAlert(e.message); } } }); indicadoresHubModal.addEventListener("change", async (event) => { const checkbox = event.target.closest("[data-dc-hub-toggle]"); if (!checkbox || checkbox.disabled) return; const chave = checkbox.getAttribute("data-dc-hub-toggle"); const atual = new Set(dcIndicadoresAtual.indicadores_selecionados || []); if (checkbox.checked) atual.add(chave); else atual.delete(chave); checkbox.disabled = true; try { await pidAtualizarIndicadoresSelecionadosContabil(apuracaoAtual.id, [...atual]); dcIndicadoresAtual = null; await renderDashboardTab(); await pidDcRenderHubIndicadores(); } catch (e) { checkbox.checked = !checkbox.checked; checkbox.disabled = false; await pidAlert(e.message); } }); function renderDashboardObservacoes() { const lista = document.getElementById("dc-dash-observacoes-list"); const empty = document.getElementById("dc-dash-observacoes-empty"); const concluida = apuracaoAtual.status === "concluida"; lista.innerHTML = ""; const itens = [ ...apuracaoAtual.contas .filter((conta) => conta.observacao) .map((conta) => ({ tipo: "conta", id: conta.id, origem: "Balancete", referencia: `${conta.codigo} — ${conta.descricao}`, texto: conta.observacao, oculto: conta.oculta_no_relatorio, })), ...apuracaoAtual.linhas_dre .filter((linha) => linha.observacao) .map((linha) => ({ tipo: "dre", id: linha.id, origem: "D.R.E.", referencia: linha.descricao, texto: linha.observacao, oculto: linha.oculta_no_relatorio, })), ...apuracaoAtual.achados .filter((achado) => achado.observacao_contador) .map((achado) => ({ tipo: "achado", id: achado.id, origem: "Auditoria", referencia: achado.titulo, texto: achado.observacao_contador, oculto: achado.oculto_no_relatorio, })), ]; empty.hidden = itens.length > 0; itens.forEach((item) => { const linha = document.createElement("div"); linha.className = `dc-dash-obs${item.oculto ? " is-oculto" : ""}`; linha.innerHTML = `
${pidDcEscapeHtml(item.origem)} ${pidDcEscapeHtml(item.referencia)}

${pidDcEscapeHtml(item.texto)}

`; 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"); });