1631 lines
71 KiB
JavaScript
1631 lines
71 KiB
JavaScript
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 = new Set();
|
||
dcDreColapsadas = 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;
|
||
}
|
||
|
||
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");
|
||
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"));
|
||
if (dcContasColapsadas.has(id)) dcContasColapsadas.delete(id);
|
||
else dcContasColapsadas.add(id);
|
||
renderContas();
|
||
return;
|
||
}
|
||
const btn = event.target.closest("[data-dc-conta-observacao]");
|
||
if (!btn) return;
|
||
abrirModalObservacao("conta", Number(btn.getAttribute("data-dc-conta-observacao")));
|
||
});
|
||
|
||
// Mesma árvore recolhível do Balancete (ver renderContas), agora sobre
|
||
// ContabilLinhaDre.nivel — que já chega pronto do backend na ordem de
|
||
// leitura do PDF, então "ter filhos" é só olhar se a próxima linha tem
|
||
// nível maior que o da linha atual.
|
||
function renderDre() {
|
||
const body = document.getElementById("dc-dre-body");
|
||
body.innerHTML = "";
|
||
const linhas = apuracaoAtual.linhas_dre;
|
||
const niveis = linhas.map((linha) => Math.max(0, linha.nivel));
|
||
const temFilhos = linhas.map((_, i) => i + 1 < linhas.length && niveis[i + 1] > niveis[i]);
|
||
|
||
const pilhaColapso = [];
|
||
linhas.forEach((linha, i) => {
|
||
const nivel = niveis[i];
|
||
while (pilhaColapso.length && pilhaColapso[pilhaColapso.length - 1] >= nivel) {
|
||
pilhaColapso.pop();
|
||
}
|
||
const oculta = pilhaColapso.length > 0;
|
||
if (temFilhos[i] && dcDreColapsadas.has(linha.id)) {
|
||
pilhaColapso.push(nivel);
|
||
}
|
||
if (oculta) return;
|
||
|
||
const expandida = !dcDreColapsadas.has(linha.id);
|
||
const toggle = temFilhos[i]
|
||
? `<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";
|
||
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"));
|
||
if (dcDreColapsadas.has(id)) dcDreColapsadas.delete(id);
|
||
else dcDreColapsadas.add(id);
|
||
renderDre();
|
||
return;
|
||
}
|
||
const btn = event.target.closest("[data-dc-dre-observacao]");
|
||
if (!btn) return;
|
||
abrirModalObservacao("dre", Number(btn.getAttribute("data-dc-dre-observacao")));
|
||
});
|
||
|
||
// ---- Tabs ----
|
||
|
||
document.getElementById("dc-tabs").addEventListener("click", async (event) => {
|
||
const tabBtn = event.target.closest("[data-dc-tab]");
|
||
if (!tabBtn) return;
|
||
const tab = tabBtn.getAttribute("data-dc-tab");
|
||
document.querySelectorAll("#dc-tabs .pa-tab").forEach((t) => t.classList.toggle("is-active", t === tabBtn));
|
||
document.querySelectorAll("[data-dc-panel]").forEach((panel) => {
|
||
panel.hidden = panel.getAttribute("data-dc-panel") !== tab;
|
||
});
|
||
if (tab === "dashboard") await renderDashboardTab();
|
||
});
|
||
|
||
// ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ----
|
||
|
||
// Indicadores só são buscados na primeira vez que a aba é aberta (mesmo
|
||
// espírito de "canvas com tamanho zero não desenha certo" do relatório em
|
||
// si — aqui o motivo é só evitar um cálculo/consulta extra ao banco em
|
||
// toda apuração aberta, já que boa parte das revisões nem chega a olhar
|
||
// esta aba). Observações (contas/DRE/achados) já estão todas carregadas
|
||
// em apuracaoAtual, não precisam de fetch próprio.
|
||
async function renderDashboardTab() {
|
||
if (!dcIndicadoresAtual) {
|
||
dcIndicadoresAtual = await pidBuscarIndicadoresContabil(apuracaoAtual.id);
|
||
}
|
||
renderDashboardIndicadores();
|
||
renderDashboardObservacoes();
|
||
}
|
||
|
||
// Agrupa as chaves de `metadados` pelo campo `grupo` de cada uma (vem do
|
||
// servidor — os 11 de sistema já têm um grupo fixo, personalizados
|
||
// sempre caem em "Indicadores Personalizados", ver
|
||
// `ContabilApuracaoViewSet.indicadores()`); só a ORDEM de exibição dos
|
||
// grupos é decidida aqui (`PID_DC_DASH_ORDEM_GRUPOS`).
|
||
function renderDashboardIndicadores() {
|
||
const container = document.getElementById("dc-dash-indicadores");
|
||
container.innerHTML = "";
|
||
if (!dcIndicadoresAtual) return;
|
||
const { indicadores, indicadores_ocultos: indicadoresOcultos, metadados } = dcIndicadoresAtual;
|
||
const concluida = apuracaoAtual.status === "concluida";
|
||
|
||
const grupos = new Map();
|
||
Object.keys(metadados).forEach((chave) => {
|
||
const nomeGrupo = metadados[chave].grupo;
|
||
if (!grupos.has(nomeGrupo)) grupos.set(nomeGrupo, []);
|
||
grupos.get(nomeGrupo).push(chave);
|
||
});
|
||
const nomesGrupos = [...grupos.keys()].sort((a, b) => {
|
||
const posicaoA = PID_DC_DASH_ORDEM_GRUPOS.indexOf(a);
|
||
const posicaoB = PID_DC_DASH_ORDEM_GRUPOS.indexOf(b);
|
||
return (posicaoA === -1 ? 99 : posicaoA) - (posicaoB === -1 ? 99 : posicaoB);
|
||
});
|
||
|
||
nomesGrupos.forEach((nomeGrupo) => {
|
||
const secao = document.createElement("div");
|
||
secao.className = "dc-dash-grupo";
|
||
secao.innerHTML = `
|
||
<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");
|
||
});
|