3476 lines
158 KiB
JavaScript
3476 lines
158 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 9 regras/chaves de regras.py (REGRAS) — enumeradas aqui pra sempre
|
||
// mostrar as 9 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: "saldo_sinal_invertido", label: "Sinal de Saldo Invertido" },
|
||
{ chave: "lucro_balancete_diverge_dre", label: "Lucro Balancete x DRE" },
|
||
{ chave: "conta_transitoria_com_saldo", label: "Contas Transitórias" },
|
||
{ chave: "conta_deveria_zerar", label: "Contas que Deveriam Zerar" },
|
||
{ chave: "descricao_generica", label: "Descrição Genérica" },
|
||
{ chave: "variacao_atipica_dre", label: "Variação Atípica na DRE" },
|
||
];
|
||
|
||
// Agrupamento temático das 9 regras acima pro resumo da aba Observações —
|
||
// evita 9 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",
|
||
"lucro_balancete_diverge_dre",
|
||
],
|
||
},
|
||
{
|
||
label: "Contas Atípicas",
|
||
regras: ["conta_transitoria_com_saldo", "conta_deveria_zerar", "descricao_generica"],
|
||
},
|
||
{
|
||
label: "Variações e Indicadores",
|
||
regras: ["variacao_atipica_dre"],
|
||
},
|
||
];
|
||
|
||
// 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>';
|
||
const PID_DC_ICON_HISTORICO =
|
||
'<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="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><path d="M12 7v5l4 2"/></svg>';
|
||
|
||
// Percentual da Análise Vertical já vem "pronto" do PDF (ex. "-7.33" quer
|
||
// dizer -7,33%, não -733%) — diferente de pidDcFormatIndicadorPercentual
|
||
// (usado pelos cards de indicador), que recebe uma fração e multiplica por
|
||
// 100 antes de exibir. Reusar aquele aqui exibiria "-733,00%" errado.
|
||
function pidDcFormatPercentualAnaliseVertical(valor) {
|
||
const numero = Number(valor || 0);
|
||
return `${numero.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%`;
|
||
}
|
||
|
||
// "mai/2026" (formato salvo pelo parser) -> "Mai/2026" (rótulo de coluna).
|
||
function pidDcFormatMesAnaliseVertical(mes) {
|
||
if (!mes) return "";
|
||
return mes.charAt(0).toUpperCase() + mes.slice(1);
|
||
}
|
||
|
||
function pidDcFormatCompetencia(iso) {
|
||
if (!iso) return "";
|
||
const [ano, mes] = iso.split("-");
|
||
return `${mes}/${ano}`;
|
||
}
|
||
|
||
function pidDcFormatData(iso) {
|
||
if (!iso) return "";
|
||
return new Date(iso).toLocaleDateString("pt-BR");
|
||
}
|
||
|
||
function pidDcEscapeHtml(texto) {
|
||
const div = document.createElement("div");
|
||
div.textContent = texto == null ? "" : String(texto);
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Posiciona o `<span class="dc-hover-tooltip__texto">` (fixed, ver
|
||
// dashboard-contabil.css) relativo à viewport, calculado no momento do
|
||
// hover/foco — nasce acima do gatilho, centralizado; sem espaço acima
|
||
// (badge perto do topo da tela) desce pra baixo do gatilho; clampado nas
|
||
// laterais pra nunca vazar pra fora da tela (badge perto da borda da
|
||
// tabela). Necessário porque os badges deste pacote vivem dentro de
|
||
// `.pa-table-wrap`, que corta (`overflow:hidden`) qualquer coisa
|
||
// posicionada com `position:absolute` que escape da própria tabela —
|
||
// `position:fixed` já resolve o corte sozinho, isto aqui só calcula onde.
|
||
// Delegado no `document` com `capture:true` porque `mouseenter`/`focus` não
|
||
// borbulham — um único listener cobre todo badge atual e futuro, mesmo os
|
||
// recriados a cada re-render da tabela.
|
||
function pidDcPosicionaTooltip(event) {
|
||
const gatilho = event.target.closest && event.target.closest(".dc-hover-tooltip__gatilho");
|
||
if (!gatilho) return;
|
||
const texto = gatilho.nextElementSibling;
|
||
if (!texto || !texto.classList.contains("dc-hover-tooltip__texto")) return;
|
||
|
||
const margem = 8;
|
||
const rect = gatilho.getBoundingClientRect();
|
||
const largura = texto.offsetWidth;
|
||
const altura = texto.offsetHeight;
|
||
|
||
let esquerda = rect.left + rect.width / 2 - largura / 2;
|
||
esquerda = Math.max(margem, Math.min(esquerda, window.innerWidth - largura - margem));
|
||
|
||
let topo = rect.top - altura - margem;
|
||
if (topo < margem) topo = rect.bottom + margem;
|
||
|
||
texto.style.left = `${esquerda}px`;
|
||
texto.style.top = `${topo}px`;
|
||
}
|
||
|
||
document.addEventListener("mouseenter", pidDcPosicionaTooltip, true);
|
||
document.addEventListener("focus", pidDcPosicionaTooltip, true);
|
||
|
||
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" });
|
||
}
|
||
|
||
async function pidReprocessarApuracaoContabil(id, formData) {
|
||
return pidApiRequest(`/contabil-apuracoes/${id}/reprocessar/`, { method: "POST", body: formData });
|
||
}
|
||
|
||
/**
|
||
* /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");
|
||
}
|
||
|
||
// ---- Observações (histórico por empresa+conta, atravessa competências) ----
|
||
// A observação não pertence mais à linha da apuração (era um campo de
|
||
// ContabilConta/ContabilLinhaDre/ContabilLinhaAnaliseVertical até a migração
|
||
// 0071) — é um registro próprio (`ContabilObservacao`), vinculado à empresa +
|
||
// chave natural da conta, que continua aparecendo nas competências seguintes
|
||
// até o contador encerrá-la. Por isso todas as chamadas abaixo são contra
|
||
// `/contabil-observacoes/`, nunca contra a linha.
|
||
|
||
async function pidBuscarObservacoesContabil(apuracaoId) {
|
||
return pidApiRequest(`/contabil-apuracoes/${apuracaoId}/observacoes/`);
|
||
}
|
||
|
||
// `alvoId` é o id da conta/linha DESTA apuração — o servidor deriva empresa,
|
||
// competência, chave natural e rótulo a partir dele (ver
|
||
// ContabilObservacaoViewSet.create).
|
||
async function pidCriarObservacaoContabil(apuracaoId, alvoTipo, alvoId, texto, mostrarAoCliente) {
|
||
return pidApiRequest("/contabil-observacoes/", {
|
||
method: "POST",
|
||
body: {
|
||
apuracao: apuracaoId,
|
||
alvo_tipo: alvoTipo,
|
||
alvo_id: alvoId,
|
||
texto,
|
||
mostrar_ao_cliente: mostrarAoCliente,
|
||
},
|
||
});
|
||
}
|
||
|
||
// `payload` aceita {texto} e/ou {mostrar_ao_cliente} — o texto só é aceito
|
||
// enquanto a observação ainda for da competência aberta; a visibilidade ao
|
||
// cliente é sempre alternável, inclusive numa observação histórica.
|
||
async function pidAtualizarObservacaoContabil(id, payload) {
|
||
return pidApiRequest(`/contabil-observacoes/${id}/`, { method: "PATCH", body: payload });
|
||
}
|
||
|
||
// "Ocultar das próximas execuções": continua visível nesta competência e
|
||
// some a partir da seguinte. `reativar` desfaz.
|
||
async function pidEncerrarObservacaoContabil(id, apuracaoId) {
|
||
return pidApiRequest(`/contabil-observacoes/${id}/encerrar/`, { method: "POST", body: { apuracao: apuracaoId } });
|
||
}
|
||
|
||
async function pidReativarObservacaoContabil(id, apuracaoId) {
|
||
return pidApiRequest(`/contabil-observacoes/${id}/reativar/`, { method: "POST", body: { apuracao: apuracaoId } });
|
||
}
|
||
|
||
// `alterada_reprocessamento` nunca é tocado por aqui (pedido explícito do
|
||
// usuário) — o badge de alerta continua visível mesmo depois de validado,
|
||
// só muda de cor (ver pidDcAlteradaBadgeHtml), pra o contador conseguir
|
||
// identificar depois quais itens já foram reprocessados E revalidados. Só
|
||
// um reprocessamento novo (sem mudança daquela conta/linha desta vez) limpa
|
||
// o campo de verdade, em `_contabil_sincroniza_*` (views.py).
|
||
async function pidAtualizarValidadoContaContabil(id, validado) {
|
||
return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: { validado } });
|
||
}
|
||
|
||
async function pidAtualizarValidadoLinhaDreContabil(id, validado) {
|
||
return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: { validado } });
|
||
}
|
||
|
||
async function pidAtualizarValidadoLinhaAnaliseVerticalContabil(id, validado) {
|
||
return pidApiRequest(`/contabil-linhas-analise-vertical/${id}/`, { method: "PATCH", body: { validado } });
|
||
}
|
||
|
||
async function pidAtualizarAchadoContabil(id, status, observacaoContador) {
|
||
return pidApiRequest(`/contabil-achados/${id}/`, {
|
||
method: "PATCH",
|
||
body: { status, observacao_contador: observacaoContador },
|
||
});
|
||
}
|
||
|
||
// ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ----
|
||
|
||
async function pidBuscarIndicadoresContabil(id) {
|
||
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores/`);
|
||
}
|
||
|
||
async function pidAtualizarIndicadoresOcultosContabil(id, indicadoresOcultos) {
|
||
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-ocultos/`, {
|
||
method: "POST",
|
||
body: { indicadores_ocultos: indicadoresOcultos },
|
||
});
|
||
}
|
||
|
||
async function pidAlternarOcultoAchadoContabil(id, oculto) {
|
||
return pidApiRequest(`/contabil-achados/${id}/alternar-oculto/`, {
|
||
method: "POST",
|
||
body: { oculto_no_relatorio: oculto },
|
||
});
|
||
}
|
||
|
||
async function pidBuscarIndicadorContabilDefinicao(id) {
|
||
return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`);
|
||
}
|
||
|
||
async function pidCriarIndicadorContabil(payload) {
|
||
return pidApiRequest("/contabil-indicadores-definicoes/", { method: "POST", body: payload });
|
||
}
|
||
|
||
async function pidAtualizarIndicadorContabil(id, payload) {
|
||
return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`, { method: "PATCH", body: payload });
|
||
}
|
||
|
||
async function pidExcluirIndicadorContabil(id) {
|
||
return pidApiRequest(`/contabil-indicadores-definicoes/${id}/`, { method: "DELETE" });
|
||
}
|
||
|
||
async function pidBuscarDefinicoesIndicadoresContabil() {
|
||
return pidApiRequest("/contabil-indicadores-definicoes/");
|
||
}
|
||
|
||
async function pidPreVisualizarIndicadorContabil(apuracaoId, payload) {
|
||
return pidApiRequest(`/contabil-apuracoes/${apuracaoId}/pre-visualizar-indicador/`, {
|
||
method: "POST",
|
||
body: payload,
|
||
});
|
||
}
|
||
|
||
async function pidAtualizarIndicadoresSelecionadosContabil(id, indicadoresSelecionados) {
|
||
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-selecionados/`, {
|
||
method: "POST",
|
||
body: { indicadores_selecionados: indicadoresSelecionados },
|
||
});
|
||
}
|
||
|
||
async function pidAtualizarResumoFechamentoContabil(id, resumoFechamento) {
|
||
return pidApiRequest(`/contabil-apuracoes/${id}/resumo-fechamento/`, {
|
||
method: "POST",
|
||
body: { resumo_fechamento: resumoFechamento },
|
||
});
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", async () => {
|
||
const listView = document.getElementById("dc-list-view");
|
||
if (!listView) return;
|
||
|
||
const me = await pidGetMe();
|
||
if (!me) return;
|
||
|
||
const relatoriosPerms = (me.permissoes_efetivas && me.permissoes_efetivas["relatorios"]) || {};
|
||
const canUse = !!(relatoriosPerms.enabled && relatoriosPerms.apps && relatoriosPerms.apps["dashboard-contabil"]);
|
||
|
||
const noAccessEl = document.getElementById("dc-no-access");
|
||
const contentEl = document.getElementById("dc-content");
|
||
if (noAccessEl) noAccessEl.hidden = canUse;
|
||
if (contentEl) contentEl.hidden = !canUse;
|
||
if (!canUse) return;
|
||
|
||
// ---- Botão "voltar ao topo" ----
|
||
// Página tem tabelas longas (Balancete/DRE/Análise Vertical, achados) —
|
||
// botão flutuante só aparece depois que o início da página já saiu da
|
||
// viewport (scroll da própria janela, `.page-content` não tem overflow
|
||
// próprio), pra não poluir a tela quando não faz falta.
|
||
const PID_DC_BACK_TO_TOP_LIMIAR_PX = 400;
|
||
const backToTopBtn = document.getElementById("dc-back-to-top-btn");
|
||
if (backToTopBtn) {
|
||
window.addEventListener("scroll", () => {
|
||
backToTopBtn.hidden = window.scrollY < PID_DC_BACK_TO_TOP_LIMIAR_PX;
|
||
});
|
||
backToTopBtn.addEventListener("click", () => {
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
});
|
||
}
|
||
|
||
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();
|
||
// Observações vigentes nesta apuração (as desta competência + as herdadas
|
||
// de competências anteriores ainda não encerradas) — carregadas à parte da
|
||
// apuração, porque não pertencem a ela: `GET /contabil-apuracoes/{id}/
|
||
// observacoes/`, ver `ContabilObservacao` no backend. `dcObsIndice` é o
|
||
// mesmo conteúdo indexado pela chave natural do alvo, pra cada linha da
|
||
// tabela achar as suas sem varrer a lista inteira.
|
||
let dcObservacoes = [];
|
||
let dcObsIndice = { conta: new Map(), dre: new Map(), analise_vertical: new Map() };
|
||
// Mesma ideia de dcDreColapsadas, agora pra árvore da Análise Vertical
|
||
// (ContabilLinhaAnaliseVertical) — mesmo nível de indentação da DRE
|
||
// (linha.nivel), só que cada linha tem um valor+percentual por mês em vez
|
||
// de um valor único.
|
||
let dcAvColapsadas = new Set();
|
||
// 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;
|
||
|
||
// Ordenação + filtro "estilo Excel" do histórico (#dc-list-table) — mesmo
|
||
// mecanismo de listSortKey/listSortDir/listFiltros em
|
||
// importacao-plano-saude.js: client-side, sobre o array já carregado
|
||
// (dcListaApuracoes), sem repaginar nem refazer fetch a cada mudança.
|
||
let dcListaApuracoes = [];
|
||
let dcListSortKey = "criado_em";
|
||
let dcListSortDir = -1;
|
||
// null = sem filtro nessa coluna; senão, um Set com os valores marcados no
|
||
// popup daquela coluna (combinam entre si por E lógico).
|
||
let dcListFiltros = { empresa: null, competencia: null, status: null, criado_por: null };
|
||
const dcListSortHeaders = Array.from(document.querySelectorAll("#dc-list-table th[data-sort]"));
|
||
|
||
function showView(view) {
|
||
listView.hidden = view !== "list";
|
||
formView.hidden = view !== "form";
|
||
reviewView.hidden = view !== "review";
|
||
}
|
||
|
||
// ---- Histórico ----
|
||
|
||
// Compara numericamente quando os dois valores parecem número (evita
|
||
// "218" ficando antes de "9" por comparação de string pura), senão cai
|
||
// pra comparação de texto (acentuada/case-insensitive) — mesma função de
|
||
// importacao-plano-saude.js (comparaValoresLista).
|
||
function dcComparaValoresLista(a, b) {
|
||
const an = Number(a);
|
||
const bn = Number(b);
|
||
const ambosNumeros = a !== "" && b !== "" && !Number.isNaN(an) && !Number.isNaN(bn);
|
||
if (ambosNumeros) return an - bn;
|
||
return String(a).localeCompare(String(b), "pt-BR", { sensitivity: "base" });
|
||
}
|
||
|
||
const PID_DC_LISTA_SORT_VALOR = {
|
||
empresa: (a) => a.codigo_empresa || "",
|
||
competencia: (a) => a.competencia || "",
|
||
observacoes: (a) => a.total_achados_pendentes ?? 0,
|
||
status: (a) => a.status || "",
|
||
criado_por: (a) => a.criado_por_nome || "",
|
||
criado_em: (a) => a.criado_em || "",
|
||
};
|
||
|
||
// Colunas com filtro "estilo Excel" no cabeçalho do histórico — mesma
|
||
// chave de PID_DC_LISTA_SORT_VALOR, reaproveitado como extrator do valor
|
||
// bruto de cada linha pro popup também (ver dcCriarFiltroColuna).
|
||
// "observacoes"/"criado_em" só ordenam, não filtram — a primeira é uma
|
||
// contagem, a segunda uma data/hora de criação, granularidade fina demais
|
||
// pra um checklist de valores distintos.
|
||
function dcLabelValorFiltroLista(campo, valor) {
|
||
if (campo === "status") return PID_DC_STATUS_LABELS[valor] || valor;
|
||
if (campo === "competencia") return pidDcFormatCompetencia(valor) || "(Vazio)";
|
||
if (campo === "empresa") {
|
||
const apuracao = dcListaApuracoes.find((a) => (a.codigo_empresa || "") === valor);
|
||
return apuracao ? `${valor || "(Vazio)"} - ${apuracao.nome_empresa}` : valor || "(Vazio)";
|
||
}
|
||
return valor || "(Vazio)";
|
||
}
|
||
|
||
function dcApuracaoPassaNosFiltros(apuracao) {
|
||
return Object.entries(dcListFiltros).every(([campo, valoresPermitidos]) => {
|
||
if (!valoresPermitidos) return true; // null = sem filtro nessa coluna
|
||
return valoresPermitidos.has(PID_DC_LISTA_SORT_VALOR[campo](apuracao));
|
||
});
|
||
}
|
||
|
||
async function carregarLista() {
|
||
dcListaApuracoes = await pidFetchApuracoesContabil();
|
||
renderList();
|
||
}
|
||
|
||
function renderList() {
|
||
const body = document.getElementById("dc-list-body");
|
||
const empty = document.getElementById("dc-list-empty");
|
||
|
||
const filtradas = dcListaApuracoes.filter(dcApuracaoPassaNosFiltros);
|
||
empty.hidden = filtradas.length !== 0;
|
||
empty.textContent =
|
||
dcListaApuracoes.length === 0
|
||
? "Nenhuma análise realizada ainda."
|
||
: "Nenhuma análise encontrada com esse filtro.";
|
||
|
||
const extrator = PID_DC_LISTA_SORT_VALOR[dcListSortKey];
|
||
const ordenadas = [...filtradas].sort((a, b) => dcComparaValoresLista(extrator(a), extrator(b)) * dcListSortDir);
|
||
|
||
body.innerHTML = "";
|
||
ordenadas.forEach((apuracao) => {
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td>${pidDcEscapeHtml(apuracao.codigo_empresa)} - ${pidDcEscapeHtml(apuracao.nome_empresa)} ${pidDcFontePdfAtipicaBadgeHtml(apuracao.fonte_pdf_atipica)}</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>
|
||
${
|
||
apuracao.status === "concluida"
|
||
? ""
|
||
: `<button type="button" class="icon-btn" data-dc-reprocessar-abrir="${apuracao.id}" aria-label="Reprocessar" title="Reprocessar (anexar outro arquivo pra esta mesma competência)">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>
|
||
</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);
|
||
});
|
||
|
||
dcListSortHeaders.forEach((th) => {
|
||
const key = th.getAttribute("data-sort");
|
||
th.classList.toggle("is-sort-asc", key === dcListSortKey && dcListSortDir === 1);
|
||
th.classList.toggle("is-sort-desc", key === dcListSortKey && dcListSortDir === -1);
|
||
});
|
||
}
|
||
|
||
dcListSortHeaders.forEach((th) => {
|
||
th.addEventListener("click", (event) => {
|
||
// Ignora cliques vindos de dentro do popup de filtro — sem isso, abrir
|
||
// o filtro ou marcar um checkbox dele também reordenava a coluna, já
|
||
// que o filtro mora dentro do mesmo <th> clicável.
|
||
if (event.target.closest(".dc-col-filter")) return;
|
||
const key = th.getAttribute("data-sort");
|
||
if (dcListSortKey === key) {
|
||
dcListSortDir *= -1;
|
||
} else {
|
||
dcListSortKey = key;
|
||
dcListSortDir = 1;
|
||
}
|
||
renderList();
|
||
});
|
||
});
|
||
|
||
// Filtro "estilo Excel" por coluna do histórico — mesmo mecanismo de
|
||
// criarFiltroColuna em importacao-plano-saude.js.
|
||
const dcFiltrosColuna = [];
|
||
|
||
function dcFecharTodosOsFiltrosColuna() {
|
||
dcFiltrosColuna.forEach((f) => f.fechar());
|
||
}
|
||
|
||
function dcEscapeAttr(value) {
|
||
return (value === null || value === undefined ? "" : String(value)).replace(/"/g, """);
|
||
}
|
||
|
||
function dcCriarFiltroColuna(campo) {
|
||
const th = dcListSortHeaders.find((el) => el.getAttribute("data-sort") === campo);
|
||
if (!th) return;
|
||
|
||
const wrapper = document.createElement("span");
|
||
wrapper.className = "dc-col-filter";
|
||
wrapper.innerHTML = `
|
||
<button type="button" class="dc-col-filter__btn" aria-label="Filtrar coluna" aria-expanded="false" title="Filtrar">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" stroke-linejoin="round"/></svg>
|
||
</button>
|
||
<div class="dc-col-filter__popup" hidden>
|
||
<input type="text" class="checklist-search" placeholder="Buscar..." />
|
||
<label class="modal-checkbox checklist-select-all"><input type="checkbox" checked /> Selecionar todos</label>
|
||
<div class="checklist-box"></div>
|
||
<div class="dc-col-filter__actions">
|
||
<button type="button" class="btn-ghost dc-col-filter__limpar">Limpar</button>
|
||
<button type="button" class="btn-solid dc-col-filter__aplicar">Aplicar</button>
|
||
</div>
|
||
</div>`;
|
||
th.appendChild(wrapper);
|
||
|
||
const btn = wrapper.querySelector(".dc-col-filter__btn");
|
||
const popup = wrapper.querySelector(".dc-col-filter__popup");
|
||
const searchInput = wrapper.querySelector(".checklist-search");
|
||
const selectAllInput = wrapper.querySelector(".checklist-select-all input");
|
||
const box = wrapper.querySelector(".checklist-box");
|
||
const limparBtn = wrapper.querySelector(".dc-col-filter__limpar");
|
||
const aplicarBtn = wrapper.querySelector(".dc-col-filter__aplicar");
|
||
const extrator = PID_DC_LISTA_SORT_VALOR[campo];
|
||
|
||
let marcados = null; // Set em edição — só existe enquanto o popup está aberto
|
||
|
||
// Ordena pelo valor bruto (dcComparaValoresLista), não pelo rótulo
|
||
// formatado — importa pra "competencia" (ISO "AAAA-MM-DD", ordena certo
|
||
// cronologicamente por string; o rótulo "MM/AAAA" ordenaria por mês
|
||
// primeiro, misturando anos).
|
||
function valoresDistintos() {
|
||
return Array.from(new Set(dcListaApuracoes.map(extrator))).sort(dcComparaValoresLista);
|
||
}
|
||
|
||
function valoresVisiveis() {
|
||
const termo = searchInput.value.trim().toLowerCase();
|
||
return valoresDistintos().filter((v) => dcLabelValorFiltroLista(campo, v).toLowerCase().includes(termo));
|
||
}
|
||
|
||
function renderItens() {
|
||
const visiveis = valoresVisiveis();
|
||
box.innerHTML = visiveis.length
|
||
? visiveis
|
||
.map(
|
||
(v) =>
|
||
`<label class="checklist-item"><input type="checkbox" data-valor-filtro="${dcEscapeAttr(v)}" ${marcados.has(v) ? "checked" : ""} /><span class="checklist-item__info">${pidDcEscapeHtml(dcLabelValorFiltroLista(campo, v))}</span></label>`
|
||
)
|
||
.join("")
|
||
: `<p class="checklist-empty">Nenhum valor encontrado.</p>`;
|
||
selectAllInput.checked = visiveis.length > 0 && visiveis.every((v) => marcados.has(v));
|
||
}
|
||
|
||
function abrir() {
|
||
// `marcados` sempre recomeça a partir do filtro já aplicado (ou de
|
||
// todos os valores existentes, se a coluna ainda não tem filtro) —
|
||
// nunca do que ficou de uma edição anterior cancelada sem "Aplicar".
|
||
marcados = dcListFiltros[campo] ? new Set(dcListFiltros[campo]) : new Set(valoresDistintos());
|
||
searchInput.value = "";
|
||
renderItens();
|
||
popup.hidden = false;
|
||
btn.setAttribute("aria-expanded", "true");
|
||
}
|
||
|
||
function fechar() {
|
||
popup.hidden = true;
|
||
marcados = null;
|
||
btn.setAttribute("aria-expanded", "false");
|
||
}
|
||
|
||
function atualizarBotaoAtivo() {
|
||
btn.classList.toggle("is-active", !!dcListFiltros[campo]);
|
||
}
|
||
|
||
btn.addEventListener("click", (event) => {
|
||
event.stopPropagation();
|
||
const estavaAberto = !popup.hidden;
|
||
dcFecharTodosOsFiltrosColuna();
|
||
if (!estavaAberto) abrir();
|
||
});
|
||
|
||
popup.addEventListener("click", (event) => event.stopPropagation());
|
||
|
||
searchInput.addEventListener("input", renderItens);
|
||
|
||
searchInput.addEventListener("keydown", (event) => {
|
||
if (event.key !== "Enter") return;
|
||
event.preventDefault();
|
||
const visiveis = valoresVisiveis();
|
||
if (visiveis.length) marcados = new Set(visiveis);
|
||
aplicarBtn.click();
|
||
});
|
||
|
||
selectAllInput.addEventListener("change", () => {
|
||
const visiveis = valoresVisiveis();
|
||
if (selectAllInput.checked) visiveis.forEach((v) => marcados.add(v));
|
||
else visiveis.forEach((v) => marcados.delete(v));
|
||
renderItens();
|
||
});
|
||
|
||
box.addEventListener("change", (event) => {
|
||
const input = event.target.closest("[data-valor-filtro]");
|
||
if (!input) return;
|
||
const valor = input.getAttribute("data-valor-filtro");
|
||
if (input.checked) marcados.add(valor);
|
||
else marcados.delete(valor);
|
||
renderItens();
|
||
});
|
||
|
||
limparBtn.addEventListener("click", () => {
|
||
dcListFiltros[campo] = null;
|
||
fechar();
|
||
atualizarBotaoAtivo();
|
||
renderList();
|
||
});
|
||
|
||
aplicarBtn.addEventListener("click", () => {
|
||
const todos = valoresDistintos();
|
||
// Marcar tudo equivale a "sem filtro" — mantém null nesse caso, senão
|
||
// um valor novo que apareça depois ficaria de fora até o usuário
|
||
// marcá-lo manualmente de novo.
|
||
const marcouTudo = todos.length === marcados.size && todos.every((v) => marcados.has(v));
|
||
dcListFiltros[campo] = marcouTudo ? null : new Set(marcados);
|
||
fechar();
|
||
atualizarBotaoAtivo();
|
||
renderList();
|
||
});
|
||
|
||
dcFiltrosColuna.push({ fechar, atualizarBotaoAtivo });
|
||
}
|
||
|
||
["empresa", "competencia", "status", "criado_por"].forEach(dcCriarFiltroColuna);
|
||
|
||
const dcListResetBtn = document.getElementById("dc-list-reset-btn");
|
||
if (dcListResetBtn) {
|
||
dcListResetBtn.addEventListener("click", () => {
|
||
dcFecharTodosOsFiltrosColuna();
|
||
dcListSortKey = "criado_em";
|
||
dcListSortDir = -1;
|
||
dcListFiltros = { empresa: null, competencia: null, status: null, criado_por: null };
|
||
dcFiltrosColuna.forEach((f) => f.atualizarBotaoAtivo());
|
||
renderList();
|
||
});
|
||
}
|
||
|
||
document.addEventListener("click", (event) => {
|
||
if (!event.target.closest(".dc-col-filter")) dcFecharTodosOsFiltrosColuna();
|
||
});
|
||
|
||
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();
|
||
}
|
||
|
||
const reprocessarBtn = event.target.closest("[data-dc-reprocessar-abrir]");
|
||
if (reprocessarBtn) {
|
||
abrirReprocessarModal(Number(reprocessarBtn.getAttribute("data-dc-reprocessar-abrir")));
|
||
}
|
||
});
|
||
|
||
// ---- Reprocessar (anexar outro PDF pra mesma empresa/competência) ----
|
||
// Diferente de "Nova Análise" (cria uma apuração do zero), reprocessar
|
||
// resincroniza a apuração já existente — o backend (`_contabil_sincroniza_*`
|
||
// em views.py) preserva observação/validação de conta/linha sem mudança e
|
||
// só reseta validado + marca `alterada_reprocessamento` no que realmente
|
||
// mudou; achados nunca são apagados nem têm status/justificativa
|
||
// sobrescritos, mesmo os que não disparam mais. Aberto direto da lista
|
||
// (não precisa abrir a apuração antes), por isso tem seu próprio `id` em
|
||
// vez de depender de `apuracaoAtual`.
|
||
const resetReprocessarArquivoField = wireArquivoField("dc-reprocessar-arquivo");
|
||
let dcReprocessarApuracaoId = null;
|
||
|
||
function abrirReprocessarModal(id) {
|
||
dcReprocessarApuracaoId = id;
|
||
document.getElementById("dc-reprocessar-arquivo").value = "";
|
||
resetReprocessarArquivoField();
|
||
document.getElementById("dc-reprocessar-error").textContent = "";
|
||
document.getElementById("dc-reprocessar-modal").hidden = false;
|
||
}
|
||
|
||
function fecharReprocessarModal() {
|
||
dcReprocessarApuracaoId = null;
|
||
document.getElementById("dc-reprocessar-modal").hidden = true;
|
||
}
|
||
|
||
document.getElementById("dc-reprocessar-cancel-btn").addEventListener("click", fecharReprocessarModal);
|
||
document.getElementById("dc-reprocessar-modal").addEventListener("click", (event) => {
|
||
if (event.target.id === "dc-reprocessar-modal") fecharReprocessarModal();
|
||
});
|
||
|
||
document.getElementById("dc-reprocessar-confirmar-btn").addEventListener("click", async () => {
|
||
const errorEl = document.getElementById("dc-reprocessar-error");
|
||
errorEl.textContent = "";
|
||
|
||
const arquivo = document.getElementById("dc-reprocessar-arquivo").files[0];
|
||
if (!arquivo) {
|
||
errorEl.textContent = "Anexe o PDF de Balancete + DRE.";
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append("arquivo", arquivo);
|
||
|
||
const confirmarBtn = document.getElementById("dc-reprocessar-confirmar-btn");
|
||
confirmarBtn.disabled = true;
|
||
try {
|
||
const atualizada = await pidReprocessarApuracaoContabil(dcReprocessarApuracaoId, formData);
|
||
fecharReprocessarModal();
|
||
await carregarLista();
|
||
// Se a apuração reprocessada é a que já está aberta na tela de
|
||
// revisão, atualiza ela também — senão o contador continuaria vendo
|
||
// dados desatualizados até sair e reabrir.
|
||
if (apuracaoAtual && apuracaoAtual.id === atualizada.id) {
|
||
apuracaoAtual = atualizada;
|
||
// As observações não são tocadas pelo reprocessamento (moram em
|
||
// `ContabilObservacao`, casadas por chave natural), mas recarrega
|
||
// junto mesmo assim: se uma conta mudou de código no PDF novo, a
|
||
// thread dela passa a pendurar em outra linha.
|
||
await dcCarregarObservacoes();
|
||
renderRevisao();
|
||
}
|
||
await pidAlert("Análise reprocessada com sucesso.");
|
||
} catch (e) {
|
||
errorEl.textContent = e.message;
|
||
} finally {
|
||
confirmarBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
// ---- 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;
|
||
await dcCarregarObservacoes();
|
||
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);
|
||
await dcCarregarObservacoes();
|
||
filtroSeveridade = "todas";
|
||
filtroStatusAchado = "pendente";
|
||
filtroRegra = null;
|
||
renderRevisao();
|
||
showView("review");
|
||
}
|
||
|
||
function renderRevisao() {
|
||
const apuracao = apuracaoAtual;
|
||
const concluida = apuracao.status === "concluida";
|
||
|
||
document.getElementById("dc-review-empresa").innerHTML =
|
||
`— ${pidDcEscapeHtml(apuracao.codigo_empresa)} - ${pidDcEscapeHtml(apuracao.nome_empresa)} ${pidDcFontePdfAtipicaBadgeHtml(apuracao.fonte_pdf_atipica)}`;
|
||
document.getElementById("dc-review-subtitle").textContent =
|
||
`CNPJ ${apuracao.cnpj || "não informado"} · Competência ${pidDcFormatCompetencia(apuracao.competencia)} · ${apuracao.contas.length} conta(s)`;
|
||
document.getElementById("dc-concluida-aviso").hidden = !concluida;
|
||
document.getElementById("dc-concluir-btn").hidden = concluida;
|
||
|
||
dcContasColapsadas = dcColapsoPadrao(apuracao.contas, dcContaNivel);
|
||
dcDreColapsadas = dcColapsoPadrao(apuracao.linhas_dre, (linha) => Math.max(0, linha.nivel));
|
||
// Destaque inicial = a última leva já visível de cada ramo (grupo
|
||
// colapsado por padrão OU folha genuína sem filho nenhum — ver
|
||
// dcUltimaLevaVisivel()) — nasce destacada sem precisar de nenhum
|
||
// clique, mesmo ajuste já feito no relatório "Gerar Dashboard" (ver
|
||
// pidDcrArvore() em dashboard-contabil-relatorio.html), pedido explícito
|
||
// do usuário pra não abrir a tela inteira na cor de grupo/total.
|
||
dcContasDestaque = dcUltimaLevaVisivel(apuracao.contas, dcContaNivel, dcContasColapsadas);
|
||
dcDreDestaque = dcUltimaLevaVisivel(apuracao.linhas_dre, (linha) => Math.max(0, linha.nivel), dcDreColapsadas);
|
||
dcContasDestaqueHistorico = [];
|
||
dcDreDestaqueHistorico = [];
|
||
dcObsPainelAberto = { conta: null, dre: null, analise_vertical: null };
|
||
dcObsEditandoId = null;
|
||
dcAchadosContaExpandida = new Set();
|
||
dcIndicadoresAtual = null;
|
||
|
||
// Aba "Análise Vertical" só existe quando o PDF tinha essa seção (nem
|
||
// todo relatório traz — ver ContabilApuracao.analise_vertical_meses) —
|
||
// some completamente quando vazia, em vez de mostrar uma tabela sem
|
||
// nenhuma coluna de mês.
|
||
const temAnaliseVertical = (apuracao.analise_vertical_meses || []).length > 0;
|
||
const avTabBtn = document.getElementById("dc-av-tab-btn");
|
||
const avEstavaAtiva = avTabBtn.classList.contains("is-active");
|
||
avTabBtn.hidden = !temAnaliseVertical;
|
||
if (!temAnaliseVertical && avEstavaAtiva) {
|
||
// A apuração anterior tinha Análise Vertical e estava nessa aba — como
|
||
// esta não tem, volta pra "Observações" pra não deixar o botão da aba
|
||
// ativa escondido com o painel dela ainda visível.
|
||
document.querySelectorAll("#dc-tabs .pa-tab").forEach((t) => t.classList.toggle("is-active", t.dataset.dcTab === "achados"));
|
||
document.querySelectorAll("[data-dc-panel]").forEach((panel) => {
|
||
panel.hidden = panel.getAttribute("data-dc-panel") !== "achados";
|
||
});
|
||
}
|
||
dcAvColapsadas = dcColapsoPadrao(apuracao.linhas_analise_vertical, (linha) => Math.max(0, linha.nivel));
|
||
dcAvDestaque = dcUltimaLevaVisivel(
|
||
apuracao.linhas_analise_vertical,
|
||
(linha) => Math.max(0, linha.nivel),
|
||
dcAvColapsadas
|
||
);
|
||
dcAvDestaqueHistorico = [];
|
||
|
||
renderAchados();
|
||
renderContas();
|
||
renderDre();
|
||
renderAnaliseVertical();
|
||
renderResumoFechamento();
|
||
}
|
||
|
||
const PID_DC_SEVERIDADE_ORDEM = { alta: 0, media: 1, baixa: 2 };
|
||
|
||
function achadosFiltrados() {
|
||
return apuracaoAtual.achados
|
||
.filter((achado) => {
|
||
if (filtroSeveridade !== "todas" && achado.severidade !== filtroSeveridade) return false;
|
||
if (filtroStatusAchado === "pendente" && achado.status !== "pendente") return false;
|
||
if (filtroRegra && achado.regra !== filtroRegra) return false;
|
||
return true;
|
||
})
|
||
.slice()
|
||
.sort((a, b) => (PID_DC_SEVERIDADE_ORDEM[a.severidade] ?? 3) - (PID_DC_SEVERIDADE_ORDEM[b.severidade] ?? 3));
|
||
}
|
||
|
||
function pidDcScrollParaLista() {
|
||
document.getElementById("dc-achados-list").scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||
}
|
||
|
||
// Resumo do topo da aba Observações: donut por severidade (clicável, filtra
|
||
// igual aos chips Alta/Média/Baixa) + grade de cards por categoria/regra
|
||
// (clicáveis, isolam achado.regra na lista abaixo). Contagens sempre sobre
|
||
// TODOS os achados da apuração, independente dos filtros já aplicados —
|
||
// visão geral estável, mesmo espírito do "Distribuição das Análises" do
|
||
// sistema externo que inspirou esta seção.
|
||
function renderAchadosResumo() {
|
||
const achados = apuracaoAtual.achados;
|
||
const total = achados.length;
|
||
|
||
// ---- Donut por severidade ----
|
||
const porSeveridade = { alta: 0, media: 0, baixa: 0 };
|
||
achados.forEach((achado) => {
|
||
if (achado.severidade in porSeveridade) porSeveridade[achado.severidade] += 1;
|
||
});
|
||
|
||
document.getElementById("dc-achados-donut-total").textContent = String(total);
|
||
|
||
const svg = document.getElementById("dc-achados-donut-svg");
|
||
const legend = document.getElementById("dc-achados-donut-legend");
|
||
legend.innerHTML = "";
|
||
|
||
let anelSvg = `<circle class="dc-donut-svg__bg" cx="18" cy="18" r="15.9155" />`;
|
||
if (total > 0) {
|
||
let acumulado = 0;
|
||
PID_DC_SEVERIDADE_LISTA.forEach((severidade) => {
|
||
const qtd = porSeveridade[severidade];
|
||
if (qtd === 0) return;
|
||
const pct = (qtd / total) * 100;
|
||
const offset = 25 - acumulado;
|
||
acumulado += pct;
|
||
anelSvg += `
|
||
<circle
|
||
class="dc-donut-svg__seg${filtroSeveridade === severidade ? " is-active" : ""}"
|
||
data-dc-donut-severidade="${severidade}"
|
||
cx="18" cy="18" r="15.9155"
|
||
stroke="${PID_DC_SEVERIDADE_CORES[severidade]}"
|
||
stroke-dasharray="${pct} ${100 - pct}"
|
||
stroke-dashoffset="${offset}"
|
||
/>
|
||
`;
|
||
});
|
||
}
|
||
svg.innerHTML = anelSvg;
|
||
svg.querySelectorAll("[data-dc-donut-severidade]").forEach((seg) => {
|
||
seg.addEventListener("click", () => {
|
||
pidDcSelecionarSeveridade(seg.getAttribute("data-dc-donut-severidade"));
|
||
pidDcScrollParaLista();
|
||
});
|
||
});
|
||
|
||
PID_DC_SEVERIDADE_LISTA.forEach((severidade) => {
|
||
const li = document.createElement("li");
|
||
li.className = `dc-donut-legend__item${filtroSeveridade === severidade ? " is-active" : ""}`;
|
||
li.innerHTML = `<span class="dc-donut-legend__dot" style="background:${PID_DC_SEVERIDADE_CORES[severidade]}"></span>${PID_DC_SEVERIDADE_LABELS[severidade]} (${porSeveridade[severidade]})`;
|
||
li.addEventListener("click", () => {
|
||
pidDcSelecionarSeveridade(severidade);
|
||
pidDcScrollParaLista();
|
||
});
|
||
legend.appendChild(li);
|
||
});
|
||
|
||
// ---- Cards por grupo temático (ver PID_DC_GRUPOS) ----
|
||
const porRegra = new Map();
|
||
PID_DC_REGRAS.forEach((r) => porRegra.set(r.chave, []));
|
||
achados.forEach((achado) => {
|
||
if (!porRegra.has(achado.regra)) porRegra.set(achado.regra, []);
|
||
porRegra.get(achado.regra).push(achado);
|
||
});
|
||
const regraPorChave = new Map(PID_DC_REGRAS.map((r) => [r.chave, r]));
|
||
|
||
const grid = document.getElementById("dc-achados-categorias");
|
||
grid.innerHTML = "";
|
||
PID_DC_GRUPOS.forEach((grupo, grupoIndex) => {
|
||
const cor = PID_DC_CATEGORIA_CORES[grupoIndex % PID_DC_CATEGORIA_CORES.length];
|
||
const totalGrupo = grupo.regras.reduce((soma, chave) => soma + (porRegra.get(chave) || []).length, 0);
|
||
|
||
const card = document.createElement("div");
|
||
card.className = "dc-categoria-card";
|
||
card.style.setProperty("--dc-categoria-cor", cor);
|
||
card.innerHTML = `
|
||
<div class="dc-categoria-card__header">
|
||
<span class="dc-categoria-card__dot"></span>
|
||
<span class="dc-categoria-card__titulo">${pidDcEscapeHtml(grupo.label)}</span>
|
||
<span class="dc-categoria-card__contagem">${totalGrupo}</span>
|
||
</div>
|
||
<div class="dc-categoria-card__regras"></div>
|
||
`;
|
||
|
||
const regrasEl = card.querySelector(".dc-categoria-card__regras");
|
||
grupo.regras.forEach((chave) => {
|
||
const regraInfo = regraPorChave.get(chave);
|
||
if (!regraInfo) return;
|
||
const itens = porRegra.get(chave) || [];
|
||
const clicavel = itens.length > 0;
|
||
const linha = document.createElement("div");
|
||
linha.className = [
|
||
"dc-categoria-card__regra",
|
||
clicavel ? "dc-categoria-card__regra--clicavel" : "dc-categoria-card__regra--vazia",
|
||
filtroRegra === chave ? "is-active" : "",
|
||
].filter(Boolean).join(" ");
|
||
linha.innerHTML = `
|
||
<span class="dc-categoria-card__regra-label">${pidDcEscapeHtml(regraInfo.label)}</span>
|
||
<span class="dc-categoria-card__regra-contagem">${itens.length}</span>
|
||
`;
|
||
if (clicavel) {
|
||
linha.addEventListener("click", () => {
|
||
filtroRegra = filtroRegra === chave ? null : chave;
|
||
renderAchados();
|
||
pidDcScrollParaLista();
|
||
});
|
||
}
|
||
regrasEl.appendChild(linha);
|
||
});
|
||
|
||
grid.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function renderAchados() {
|
||
renderAchadosResumo();
|
||
|
||
const filtroAtivo = document.getElementById("dc-regra-filtro-ativo");
|
||
const regraInfo = filtroRegra ? PID_DC_REGRAS.find((r) => r.chave === filtroRegra) : null;
|
||
filtroAtivo.hidden = !regraInfo;
|
||
if (regraInfo) document.getElementById("dc-regra-filtro-ativo-label").textContent = regraInfo.label;
|
||
|
||
const lista = document.getElementById("dc-achados-list");
|
||
const empty = document.getElementById("dc-achados-empty");
|
||
const achados = achadosFiltrados();
|
||
lista.innerHTML = "";
|
||
empty.hidden = achados.length > 0;
|
||
|
||
achados.forEach((achado) => {
|
||
const card = document.createElement("div");
|
||
card.className = `dc-achado-card dc-achado-card--${achado.severidade}`;
|
||
const podeEditar = apuracaoAtual.status !== "concluida" && achado.status === "pendente";
|
||
const conta = achado.conta != null ? apuracaoAtual.contas.find((c) => c.id === achado.conta) : null;
|
||
const expandida = dcAchadosContaExpandida.has(achado.id);
|
||
card.innerHTML = `
|
||
<div class="dc-achado-card__main">
|
||
<div class="dc-achado-card__titulo">
|
||
<span class="dc-badge dc-badge--${achado.severidade}">${PID_DC_SEVERIDADE_LABELS[achado.severidade] || achado.severidade}</span>
|
||
<span class="dc-badge dc-badge--${achado.status}">${PID_DC_ACHADO_STATUS_LABELS[achado.status] || achado.status}</span>
|
||
${pidDcEscapeHtml(achado.titulo)}
|
||
</div>
|
||
<div class="dc-achado-card__mensagem">${pidDcEscapeHtml(achado.mensagem)}</div>
|
||
${conta ? `
|
||
<button type="button" class="dc-achado-card__toggle-conta${expandida ? " is-expanded" : ""}" data-dc-achado-toggle-conta="${achado.id}">
|
||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||
${expandida ? "Ocultar conta usada no apontamento" : "Ver conta usada no apontamento"}
|
||
</button>
|
||
${expandida ? `
|
||
<dl class="dc-achado-card__conta">
|
||
<div><dt>Conta</dt><dd>${pidDcEscapeHtml(conta.codigo)} — ${pidDcEscapeHtml(conta.descricao)}</dd></div>
|
||
<div><dt>Saldo Anterior</dt><dd>${pidDcFormatMoeda(conta.saldo_anterior)}</dd></div>
|
||
<div><dt>Débito</dt><dd>${pidDcFormatMoeda(conta.debito)}</dd></div>
|
||
<div><dt>Crédito</dt><dd>${pidDcFormatMoeda(conta.credito)}</dd></div>
|
||
<div><dt>Saldo Atual</dt><dd>${pidDcFormatMoeda(conta.saldo_atual)}</dd></div>
|
||
</dl>
|
||
` : ""}
|
||
` : ""}
|
||
${achado.observacao_contador ? `<div class="dc-achado-card__observacao">${pidDcEscapeHtml(achado.observacao_contador)}${achado.tratado_por_nome ? ` — ${pidDcEscapeHtml(achado.tratado_por_nome)}` : ""}</div>` : ""}
|
||
</div>
|
||
<div class="dc-achado-card__actions">
|
||
${podeEditar ? `<button type="button" class="btn-outline" data-dc-achado-editar="${achado.id}">Revisar</button>` : ""}
|
||
</div>
|
||
`;
|
||
lista.appendChild(card);
|
||
});
|
||
}
|
||
|
||
document.getElementById("dc-achados-list").addEventListener("click", (event) => {
|
||
const toggleBtn = event.target.closest("[data-dc-achado-toggle-conta]");
|
||
if (toggleBtn) {
|
||
const id = Number(toggleBtn.getAttribute("data-dc-achado-toggle-conta"));
|
||
if (dcAchadosContaExpandida.has(id)) dcAchadosContaExpandida.delete(id);
|
||
else dcAchadosContaExpandida.add(id);
|
||
renderAchados();
|
||
return;
|
||
}
|
||
const btn = event.target.closest("[data-dc-achado-editar]");
|
||
if (!btn) return;
|
||
abrirModalAchado(Number(btn.getAttribute("data-dc-achado-editar")));
|
||
});
|
||
|
||
// Reaproveitado tanto pelos chips de severidade quanto pelo clique no
|
||
// donut/legenda do resumo (ver renderAchadosResumo) — mesmo efeito de
|
||
// clicar o chip correspondente, sem duplicar a lógica de estado+re-render.
|
||
function pidDcSelecionarSeveridade(severidade) {
|
||
filtroSeveridade = severidade;
|
||
document
|
||
.querySelectorAll("#dc-achados-severidade-chips .dc-chip")
|
||
.forEach((c) => c.classList.toggle("is-active", c.getAttribute("data-dc-severidade") === severidade));
|
||
renderAchados();
|
||
}
|
||
|
||
document.querySelectorAll("#dc-achados-severidade-chips [data-dc-severidade]").forEach((chip) => {
|
||
chip.addEventListener("click", () => pidDcSelecionarSeveridade(chip.getAttribute("data-dc-severidade")));
|
||
});
|
||
|
||
document.querySelectorAll("#dc-achados-status-chips [data-dc-status]").forEach((chip) => {
|
||
chip.addEventListener("click", () => {
|
||
filtroStatusAchado = chip.getAttribute("data-dc-status");
|
||
document.querySelectorAll("#dc-achados-status-chips .dc-chip").forEach((c) => c.classList.toggle("is-active", c === chip));
|
||
renderAchados();
|
||
});
|
||
});
|
||
|
||
document.getElementById("dc-regra-filtro-limpar-btn").addEventListener("click", () => {
|
||
filtroRegra = null;
|
||
renderAchados();
|
||
});
|
||
|
||
// O código da conta (ex. "1.01.01.002.001") já chega do backend na mesma
|
||
// ordem hierárquica/sequencial do PDF (uma sintética seguida das suas
|
||
// analíticas, ver portal_api/dashboard_contabil/CLAUDE.md) — o nível de
|
||
// indentação vem direto da contagem de segmentos separados por ".", sem
|
||
// precisar reconstruir uma árvore por id/parent (o mesmo código pode se
|
||
// repetir em várias linhas irmãs, ex. três bancos diferentes sob o mesmo
|
||
// "1.01.01.002.001", então não dá pra usar o código como chave única).
|
||
function dcContaNivel(conta) {
|
||
return conta.codigo.split(".").length - 1;
|
||
}
|
||
|
||
// A partir deste nível (equivalente ao 3º segmento do código de
|
||
// classificação, ex. "1.01.01") o Balancete/DRE nascem recolhidos —
|
||
// pedido explícito do usuário pra reduzir a poluição visual da árvore
|
||
// completa; o contador expande sob demanda pra ver o nível analítico
|
||
// abaixo. Mesmo limiar usado no relatório "Gerar Dashboard"
|
||
// (_CONTABIL_NIVEL_ABERTO_PADRAO em views.py).
|
||
const PID_DC_NIVEL_ABERTO_PADRAO = 2;
|
||
|
||
function dcColapsoPadrao(itens, nivelFn) {
|
||
const niveis = itens.map(nivelFn);
|
||
const colapsadas = new Set();
|
||
itens.forEach((item, i) => {
|
||
const temFilhos = i + 1 < itens.length && niveis[i + 1] > niveis[i];
|
||
if (temFilhos && niveis[i] >= PID_DC_NIVEL_ABERTO_PADRAO) colapsadas.add(item.id);
|
||
});
|
||
return colapsadas;
|
||
}
|
||
|
||
// Destaque inicial (antes de qualquer clique) — pedido explícito do
|
||
// usuário depois de notar que folhas genuínas (conta/linha sem filho
|
||
// nenhum, ex. "(-) SIMPLES NACIONAL") ficavam de fora do destaque mesmo
|
||
// sendo visualmente tão "de baixo" quanto um grupo colapsado por padrão
|
||
// no mesmo nível. Usar só `colapsadas` (que só marca linhas que TÊM
|
||
// filhos escondidos) deixa de fora exatamente essas folhas, já que elas
|
||
// nunca entram nesse conjunto (não têm filho pra esconder).
|
||
//
|
||
// Critério correto, que cobre os dois casos com a mesma regra: entre as
|
||
// linhas realmente visíveis com este `colapsadas`, uma linha entra no
|
||
// destaque se a próxima linha visível **não** for mais profunda que ela
|
||
// — ou seja, nada foi revelado logo abaixo dela agora, seja porque ela
|
||
// está colapsada (filhos escondidos) ou porque é uma folha sem filho.
|
||
function dcUltimaLevaVisivel(itens, nivelFn, colapsadas) {
|
||
const niveis = itens.map(nivelFn);
|
||
const temFilhos = itens.map((_, i) => i + 1 < itens.length && niveis[i + 1] > niveis[i]);
|
||
const visiveis = [];
|
||
const pilha = [];
|
||
itens.forEach((item, i) => {
|
||
const nivel = niveis[i];
|
||
while (pilha.length && pilha[pilha.length - 1] >= nivel) pilha.pop();
|
||
const oculta = pilha.length > 0;
|
||
const temFilhoOculto = temFilhos[i] && colapsadas.has(item.id);
|
||
if (temFilhoOculto) pilha.push(nivel);
|
||
if (!oculta) visiveis.push({ id: item.id, nivel, temFilhoOculto });
|
||
});
|
||
const destaque = new Set();
|
||
visiveis.forEach((visivel, i) => {
|
||
const proximo = visiveis[i + 1];
|
||
const eraFimDoRamo = !proximo || proximo.nivel <= visivel.nivel;
|
||
if (!eraFimDoRamo) return;
|
||
// Nível 0 só entra no destaque quando realmente esconde algo (grupo
|
||
// colapsado manualmente) — pedido explícito do usuário: uma sequência
|
||
// de linhas de resultado irmãs no nível raiz da DRE (ex. "RESULTADO
|
||
// ANTES DA CS E IR", "(=) PREJUÍZO LÍQUIDO DO EXERCÍCIO") não é o fim
|
||
// de um ramo aninhado, são linhas soltas lado a lado — não deviam
|
||
// herdar o destaque pensado pra folha/grupo aninhado.
|
||
if (visivel.nivel === 0 && !visivel.temFilhoOculto) return;
|
||
destaque.add(visivel.id);
|
||
});
|
||
return destaque;
|
||
}
|
||
|
||
// Ids da última "leva" de linhas recém-reveladas por um clique de
|
||
// expandir — pedido explícito do usuário: destacar as contas que
|
||
// aparecem ao expandir um grupo, e mover o destaque (não acumular) pra a
|
||
// leva mais recente conforme o contador vai expandindo mais fundo. Cada
|
||
// clique de expandir SUBSTITUI esse conjunto, empilhando o que estava
|
||
// destacado antes em dc*DestaqueHistorico; um clique de recolher desempilha
|
||
// e restaura o destaque anterior (pedido explícito do usuário — fechar uma
|
||
// leva revela de novo o destaque de onde se estava antes de abri-la, em vez
|
||
// de simplesmente apagar o destaque). Um par conjunto+pilha por árvore, já
|
||
// que Balancete e DRE são independentes.
|
||
let dcContasDestaque = new Set();
|
||
let dcDreDestaque = new Set();
|
||
let dcAvDestaque = new Set();
|
||
let dcContasDestaqueHistorico = [];
|
||
let dcDreDestaqueHistorico = [];
|
||
let dcAvDestaqueHistorico = [];
|
||
|
||
// Filhos DIRETOS de um item (mesmo próximo nível, não os netos) — usado
|
||
// pra saber quais linhas acabaram de ficar visíveis ao expandir `id`.
|
||
function dcFilhosDiretos(itens, nivelFn, id) {
|
||
const niveis = itens.map(nivelFn);
|
||
const idx = itens.findIndex((item) => item.id === id);
|
||
const filhos = new Set();
|
||
if (idx === -1) return filhos;
|
||
const nivelPai = niveis[idx];
|
||
for (let j = idx + 1; j < itens.length; j++) {
|
||
if (niveis[j] <= nivelPai) break;
|
||
if (niveis[j] === nivelPai + 1) filhos.add(itens[j].id);
|
||
}
|
||
return filhos;
|
||
}
|
||
|
||
// TODOS os descendentes de um item (não só os diretos — filhos, netos,
|
||
// bisnetos...) — usado pelo estado tri-state do botão "validado" de uma
|
||
// conta/linha sintética (com filhos), ver dcEstadoValidacaoGrupo() logo
|
||
// abaixo e o clique em [data-dc-conta-validado]/[data-dc-dre-validado].
|
||
// `somenteFolhas` restringe aos descendentes SEM filhos (contas/linhas
|
||
// reais, nunca sintéticas) — usado pelo cálculo de "completo" de
|
||
// dcEstadoValidacaoGrupo (ver comentário lá) pra ignorar o campo
|
||
// `validado` de um descendente sintético intermediário, que nunca é
|
||
// marcado sozinho quando só os netos (folhas) são validados um a um.
|
||
function dcDescendentes(itens, nivelFn, id, somenteFolhas = false) {
|
||
const niveis = itens.map(nivelFn);
|
||
const idx = itens.findIndex((item) => item.id === id);
|
||
const descendentes = [];
|
||
if (idx === -1) return descendentes;
|
||
const nivelPai = niveis[idx];
|
||
for (let j = idx + 1; j < itens.length; j++) {
|
||
if (niveis[j] <= nivelPai) break;
|
||
const temFilho = j + 1 < itens.length && niveis[j + 1] > niveis[j];
|
||
if (!somenteFolhas || !temFilho) descendentes.push(itens[j]);
|
||
}
|
||
return descendentes;
|
||
}
|
||
|
||
// Verdadeiro se algum descendente (filho, neto...) de uma conta/linha
|
||
// sintética tem observação vigente — usado pra destacar o ícone de
|
||
// observação de um grupo recolhido mesmo quando a própria sintética não
|
||
// tem observação própria (pedido explícito do usuário: "caso esteja
|
||
// recolhida, o usuário consegue visualizar se há ou não observações
|
||
// realizadas" nas contas/linhas dentro dela).
|
||
function dcTemObservacaoDescendente(tipo, itens, nivelFn, chaveFn, id) {
|
||
return dcDescendentes(itens, nivelFn, id).some((item) => dcObservacoesDe(tipo, chaveFn(item)).length > 0);
|
||
}
|
||
|
||
// Estado do botão "validado" de uma conta/linha COM filhos (sintética) —
|
||
// pedido explícito do usuário: "nenhum" (cor padrão) quando nem ela nem
|
||
// nenhum descendente está validado; "completo" (verde, mesma cor de uma
|
||
// folha validada) quando TODO descendente está validado; "parcial"
|
||
// (amarelo) em qualquer combinação intermediária — inclui o caso de só a
|
||
// própria sintética ter sido marcada (primeiro clique), sinalizando "em
|
||
// andamento" até os descendentes acompanharem.
|
||
//
|
||
// "completo" usa só os descendentes FOLHA (`descendentesFolhas`, sem
|
||
// filhos), não todo `descendentes` (que inclui sintéticas intermediárias
|
||
// — ex.: uma "mãe" com "filhas" que por sua vez têm "filhos"/netos): o
|
||
// campo `validado` de uma sintética intermediária nunca é marcado sozinho
|
||
// quando o usuário valida os netos um a um direto (sem clicar na própria
|
||
// filha) — usar todo `descendentes` nesse caso fazia a mãe nunca virar
|
||
// "completo" mesmo com todos os netos validados, já que a filha
|
||
// intermediária ficava com `validado=false` na checagem. `descendentes`
|
||
// (todo a árvore) continua valendo pro "parcial" — uma sintética marcada
|
||
// sozinha (sem cascatear) ainda deve sinalizar "em andamento" no ancestral.
|
||
function dcEstadoValidacaoGrupo(item, descendentes, descendentesFolhas) {
|
||
if (descendentesFolhas.length && descendentesFolhas.every((d) => d.validado)) return "completo";
|
||
if (item.validado || descendentes.some((d) => d.validado)) return "parcial";
|
||
return "nenhum";
|
||
}
|
||
|
||
// Classe/título/aria do botão "validado" de uma linha — folha (`!temFilhosItem`)
|
||
// continua o toggle simples de sempre; sintética (com filhos) usa o
|
||
// tri-state de dcEstadoValidacaoGrupo (nenhum/parcial/completo).
|
||
function dcValidadoInfo(item, itens, nivelFn, temFilhosItem) {
|
||
if (!temFilhosItem) {
|
||
return {
|
||
classe: item.validado ? " dc-conta-validado-btn--validado" : "",
|
||
title: item.validado ? "Validada — clique para desmarcar" : "Marcar como validada",
|
||
label: item.validado ? "Desmarcar validação" : "Marcar como validada",
|
||
pressed: item.validado,
|
||
};
|
||
}
|
||
const estado = dcEstadoValidacaoGrupo(
|
||
item,
|
||
dcDescendentes(itens, nivelFn, item.id),
|
||
dcDescendentes(itens, nivelFn, item.id, true)
|
||
);
|
||
if (estado === "completo") {
|
||
return {
|
||
classe: " dc-conta-validado-btn--validado",
|
||
title: "Grupo totalmente validado — clique para desmarcar tudo",
|
||
label: "Desmarcar validação de todo o grupo",
|
||
pressed: true,
|
||
};
|
||
}
|
||
if (estado === "parcial") {
|
||
return {
|
||
classe: " dc-conta-validado-btn--parcial",
|
||
title: "Algumas contas do grupo já validadas — clique para validar todas",
|
||
label: "Validar todas as contas do grupo",
|
||
pressed: false,
|
||
};
|
||
}
|
||
return { classe: "", title: "Marcar como validada", label: "Marcar como validada", pressed: false };
|
||
}
|
||
|
||
// Clique no botão "validado" de uma conta do Balancete — pedido explícito
|
||
// do usuário: numa folha (sem filhos) é só um toggle simples; numa
|
||
// sintética (com filhos) o clique se comporta em 3 etapas conforme o
|
||
// estado atual do grupo (ver dcEstadoValidacaoGrupo):
|
||
// "nenhum" → marca só a própria sintética (passa a "parcial").
|
||
// "parcial" → pergunta antes de validar TODAS as contas do grupo de
|
||
// uma vez (passa a "completo").
|
||
// "completo" → desmarca a própria sintética + todas as contas do grupo,
|
||
// sem perguntar (mesmo pedido explícito: "apertar
|
||
// novamente desmarca todo o grupo").
|
||
// Sempre termina com renderContas() — inclusive no caso simples de folha —
|
||
// pra recalcular a cor de qualquer sintética ancestral que dependa deste
|
||
// item (o estado dela é derivado dos descendentes a cada render, nunca
|
||
// armazenado à parte).
|
||
async function dcClicarValidadoConta(id) {
|
||
const contas = apuracaoAtual.contas;
|
||
const idx = contas.findIndex((c) => c.id === id);
|
||
if (idx === -1) return;
|
||
const niveis = contas.map(dcContaNivel);
|
||
const temFilhosItem = idx + 1 < contas.length && niveis[idx + 1] > niveis[idx];
|
||
const conta = contas[idx];
|
||
|
||
if (!temFilhosItem) {
|
||
const atualizada = await pidAtualizarValidadoContaContabil(id, !conta.validado);
|
||
contas[idx] = atualizada;
|
||
renderContas();
|
||
return;
|
||
}
|
||
|
||
const descendentes = dcDescendentes(contas, dcContaNivel, id);
|
||
const descendentesFolhas = dcDescendentes(contas, dcContaNivel, id, true);
|
||
const estado = dcEstadoValidacaoGrupo(conta, descendentes, descendentesFolhas);
|
||
|
||
if (estado === "completo") {
|
||
const alvos = [conta, ...descendentes].filter((item) => item.validado);
|
||
const respostas = await Promise.all(alvos.map((item) => pidAtualizarValidadoContaContabil(item.id, false)));
|
||
respostas.forEach((atualizada) => {
|
||
const i = contas.findIndex((c) => c.id === atualizada.id);
|
||
if (i >= 0) contas[i] = atualizada;
|
||
});
|
||
renderContas();
|
||
return;
|
||
}
|
||
|
||
if (estado === "parcial") {
|
||
if (!(await pidConfirm("Deseja validar todas as contas deste grupo?"))) return;
|
||
const alvos = [conta, ...descendentes].filter((item) => !item.validado);
|
||
const respostas = await Promise.all(alvos.map((item) => pidAtualizarValidadoContaContabil(item.id, true)));
|
||
respostas.forEach((atualizada) => {
|
||
const i = contas.findIndex((c) => c.id === atualizada.id);
|
||
if (i >= 0) contas[i] = atualizada;
|
||
});
|
||
renderContas();
|
||
return;
|
||
}
|
||
|
||
const atualizada = await pidAtualizarValidadoContaContabil(id, true);
|
||
contas[idx] = atualizada;
|
||
renderContas();
|
||
}
|
||
|
||
// Mesma lógica de dcClicarValidadoConta(), sobre ContabilLinhaDre.
|
||
async function dcClicarValidadoLinha(id) {
|
||
const linhas = apuracaoAtual.linhas_dre;
|
||
const nivelFn = (l) => Math.max(0, l.nivel);
|
||
const idx = linhas.findIndex((l) => l.id === id);
|
||
if (idx === -1) return;
|
||
const niveis = linhas.map(nivelFn);
|
||
const temFilhosItem = idx + 1 < linhas.length && niveis[idx + 1] > niveis[idx];
|
||
const linha = linhas[idx];
|
||
|
||
if (!temFilhosItem) {
|
||
const atualizada = await pidAtualizarValidadoLinhaDreContabil(id, !linha.validado);
|
||
linhas[idx] = atualizada;
|
||
renderDre();
|
||
return;
|
||
}
|
||
|
||
const descendentes = dcDescendentes(linhas, nivelFn, id);
|
||
const descendentesFolhas = dcDescendentes(linhas, nivelFn, id, true);
|
||
const estado = dcEstadoValidacaoGrupo(linha, descendentes, descendentesFolhas);
|
||
|
||
if (estado === "completo") {
|
||
const alvos = [linha, ...descendentes].filter((item) => item.validado);
|
||
const respostas = await Promise.all(alvos.map((item) => pidAtualizarValidadoLinhaDreContabil(item.id, false)));
|
||
respostas.forEach((atualizada) => {
|
||
const i = linhas.findIndex((l) => l.id === atualizada.id);
|
||
if (i >= 0) linhas[i] = atualizada;
|
||
});
|
||
renderDre();
|
||
return;
|
||
}
|
||
|
||
if (estado === "parcial") {
|
||
if (!(await pidConfirm("Deseja validar todas as contas deste grupo?"))) return;
|
||
const alvos = [linha, ...descendentes].filter((item) => !item.validado);
|
||
const respostas = await Promise.all(alvos.map((item) => pidAtualizarValidadoLinhaDreContabil(item.id, true)));
|
||
respostas.forEach((atualizada) => {
|
||
const i = linhas.findIndex((l) => l.id === atualizada.id);
|
||
if (i >= 0) linhas[i] = atualizada;
|
||
});
|
||
renderDre();
|
||
return;
|
||
}
|
||
|
||
const atualizada = await pidAtualizarValidadoLinhaDreContabil(id, true);
|
||
linhas[idx] = atualizada;
|
||
renderDre();
|
||
}
|
||
|
||
// Mesma lógica de dcClicarValidadoConta()/dcClicarValidadoLinha(), sobre
|
||
// ContabilLinhaAnaliseVertical.
|
||
async function dcClicarValidadoAv(id) {
|
||
const linhas = apuracaoAtual.linhas_analise_vertical;
|
||
const nivelFn = (l) => Math.max(0, l.nivel);
|
||
const idx = linhas.findIndex((l) => l.id === id);
|
||
if (idx === -1) return;
|
||
const niveis = linhas.map(nivelFn);
|
||
const temFilhosItem = idx + 1 < linhas.length && niveis[idx + 1] > niveis[idx];
|
||
const linha = linhas[idx];
|
||
|
||
if (!temFilhosItem) {
|
||
const atualizada = await pidAtualizarValidadoLinhaAnaliseVerticalContabil(id, !linha.validado);
|
||
linhas[idx] = atualizada;
|
||
renderAnaliseVertical();
|
||
return;
|
||
}
|
||
|
||
const descendentes = dcDescendentes(linhas, nivelFn, id);
|
||
const descendentesFolhas = dcDescendentes(linhas, nivelFn, id, true);
|
||
const estado = dcEstadoValidacaoGrupo(linha, descendentes, descendentesFolhas);
|
||
|
||
if (estado === "completo") {
|
||
const alvos = [linha, ...descendentes].filter((item) => item.validado);
|
||
const respostas = await Promise.all(
|
||
alvos.map((item) => pidAtualizarValidadoLinhaAnaliseVerticalContabil(item.id, false))
|
||
);
|
||
respostas.forEach((atualizada) => {
|
||
const i = linhas.findIndex((l) => l.id === atualizada.id);
|
||
if (i >= 0) linhas[i] = atualizada;
|
||
});
|
||
renderAnaliseVertical();
|
||
return;
|
||
}
|
||
|
||
if (estado === "parcial") {
|
||
if (!(await pidConfirm("Deseja validar todas as contas deste grupo?"))) return;
|
||
const alvos = [linha, ...descendentes].filter((item) => !item.validado);
|
||
const respostas = await Promise.all(
|
||
alvos.map((item) => pidAtualizarValidadoLinhaAnaliseVerticalContabil(item.id, true))
|
||
);
|
||
respostas.forEach((atualizada) => {
|
||
const i = linhas.findIndex((l) => l.id === atualizada.id);
|
||
if (i >= 0) linhas[i] = atualizada;
|
||
});
|
||
renderAnaliseVertical();
|
||
return;
|
||
}
|
||
|
||
const atualizada = await pidAtualizarValidadoLinhaAnaliseVerticalContabil(id, true);
|
||
linhas[idx] = atualizada;
|
||
renderAnaliseVertical();
|
||
}
|
||
|
||
// Conteúdo do botão de observação de uma conta/linha — pedido explícito
|
||
// do usuário: a coluna "Observação" ficava poluída visualmente com
|
||
// "+ Observação"/o texto da observação em si repetido em toda linha;
|
||
// virou sempre o mesmo ícone, só a cor muda
|
||
// (`.dc-conta-observacao-btn--preenchida`, ver dashboard-contabil.css) pra
|
||
// indicar de relance quais linhas já têm observação, com um contador ao
|
||
// lado (ver dcObsBotaoHtml). Clicar não abre um popup — abre o painel de
|
||
// observações dentro da própria tabela (`<tr class="dc-obs-edit-row">`),
|
||
// pedido explícito do usuário pra não perder o texto por causa de um
|
||
// clique fora de um modal.
|
||
const PID_DC_OBSERVACAO_ICONE =
|
||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
|
||
|
||
// Botão de "validado" (check) ao lado do de observação — pedido explícito
|
||
// do usuário, um segundo controle puramente informativo pro contador
|
||
// marcar que já conferiu aquela conta/linha durante a revisão (não afeta
|
||
// achados/observação/relatório, só o próprio marcador).
|
||
const PID_DC_VALIDADO_ICONE =
|
||
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>';
|
||
|
||
// Tooltip customizado no visual do Portal (ver `.dc-hover-tooltip` em
|
||
// dashboard-contabil.css) — troca o balão nativo do navegador (que mostra
|
||
// o `title`, sem estilo nenhum do app) por um `<span>` posicionado via CSS
|
||
// puro, pedido explícito do usuário depois de ver o tooltip nativo nos
|
||
// badges abaixo. `gatilhoHtml` é o ícone (já pronto, com sua própria
|
||
// classe/cor); `texto` vai só no `<span>` do tooltip (nunca em `title`,
|
||
// que voltaria a mostrar o balão nativo do navegador em cima do custom).
|
||
function pidDcHoverTooltipHtml(gatilhoHtml, gatilhoClasse, texto) {
|
||
return `<span class="dc-hover-tooltip">
|
||
<span class="dc-hover-tooltip__gatilho ${gatilhoClasse}" tabindex="0" aria-label="${pidDcEscapeHtml(texto)}">${gatilhoHtml}</span>
|
||
<span class="dc-hover-tooltip__texto" role="tooltip">${pidDcEscapeHtml(texto)}</span>
|
||
</span>`;
|
||
}
|
||
|
||
// Badge de alerta ao lado dos botões de validado/observação — marca uma
|
||
// conta/linha que mudou no último reprocessamento (`alterada_reprocessamento`,
|
||
// ver `ContabilApuracaoViewSet.reprocessar()`/`_contabil_sincroniza_*` em
|
||
// views.py). Puramente informativo (não clicável) — permanece visível
|
||
// mesmo depois do contador marcar a conta/linha como validada de novo
|
||
// (pedido explícito do usuário: precisa dar pra identificar depois quais
|
||
// itens já foram reprocessados E revalidados, não só sumir o aviso), só
|
||
// muda de cor (`--danger` enquanto pendente, verde quando já validado) —
|
||
// some de vez só num próximo reprocessamento sem essa mudança específica.
|
||
const PID_DC_ALTERADA_ICONE =
|
||
'<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="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><path d="M12 9v4M12 17h.01"/></svg>';
|
||
|
||
// `valorAnteriorTexto` (já formatado pelo chamador — pidDcFormatMoeda() pra
|
||
// Balancete/DRE, pidDcValorAnteriorAvTexto() pra Análise Vertical) some no
|
||
// tooltip pra visualização rápida no hover, sem precisar abrir a apuração
|
||
// anterior — pedido explícito do usuário. `null` quando o backend não tem
|
||
// o valor anterior salvo (conta/linha nova, sem "antes" pra comparar).
|
||
function pidDcAlteradaBadgeHtml(alterada, validado, valorAnteriorTexto) {
|
||
if (!alterada) return "";
|
||
const classe = validado ? "dc-conta-alterada-badge dc-conta-alterada-badge--validada" : "dc-conta-alterada-badge";
|
||
const situacao = validado
|
||
? "Esta conta/linha mudou no último reprocessamento e já foi revalidada."
|
||
: "Esta conta/linha mudou no último reprocessamento e voltou a ficar não validada.";
|
||
const texto = valorAnteriorTexto ? `${situacao}\nValor antes do reprocessamento: ${valorAnteriorTexto}` : situacao;
|
||
return pidDcHoverTooltipHtml(PID_DC_ALTERADA_ICONE, classe, texto);
|
||
}
|
||
|
||
// Só usado pela Análise Vertical (Balancete/DRE têm um valor único —
|
||
// formatam com pidDcFormatMoeda() direto no chamador) — junta o valor
|
||
// antes do reprocessamento de cada mês (mesmo alinhamento posicional de
|
||
// ContabilApuracao.analise_vertical_meses/linha.valores, ver o model) numa
|
||
// linha por mês, uma por `\n` (o atributo `title` já quebra linha nisso).
|
||
function pidDcValorAnteriorAvTexto(linha) {
|
||
const anteriores = linha.valores_anterior_reprocessamento;
|
||
if (!anteriores || !anteriores.length) return null;
|
||
const meses = apuracaoAtual.analise_vertical_meses || [];
|
||
return anteriores
|
||
.map((v, i) => `${pidDcFormatMesAnaliseVertical(meses[i])}: ${pidDcFormatMoeda(v.valor)} (${pidDcFormatPercentualAnaliseVertical(v.percentual)})`)
|
||
.join("\n");
|
||
}
|
||
|
||
// Aviso de "fonte de PDF atípica" (ContabilApuracao.fonte_pdf_atipica,
|
||
// detectado em parser.py — ver _normaliza_titulo()/fonte_pdf_atipica) ao
|
||
// lado do nome da empresa, tanto na lista quanto no cabeçalho da revisão.
|
||
// Puramente informativo: avisa que esse PDF específico usa uma fonte
|
||
// diferente da do relatório de referência, o que já causou palavras
|
||
// grudadas em algumas descrições de conta em pelo menos um caso real
|
||
// (confirmado contra `1751 - Balancete 07.2026.pdf`) — não há correção
|
||
// automática segura pra isso (ver CLAUDE.md do pacote), então o jeito é
|
||
// avisar o contador pra conferir com atenção os nomes de conta dessa
|
||
// apuração específica.
|
||
const PID_DC_FONTE_ATIPICA_ICONE =
|
||
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
|
||
|
||
function pidDcFontePdfAtipicaBadgeHtml(fontePdfAtipica) {
|
||
if (!fontePdfAtipica) return "";
|
||
const texto =
|
||
"Este relatório foi gerado a partir de um modelo de PDF diferente do padrão — pequenos erros de nomenclatura podem ocorrer em algumas contas/linhas devido à fonte usada neste documento. Vale conferir os nomes com atenção.";
|
||
return pidDcHoverTooltipHtml(PID_DC_FONTE_ATIPICA_ICONE, "dc-fonte-atipica-badge", texto);
|
||
}
|
||
|
||
// ---- Observações: histórico por empresa+conta (ContabilObservacao) ----
|
||
// A observação deixou de ser um campo da linha e virou uma thread por
|
||
// conta/linha que atravessa competências: o que o contador escreveu em
|
||
// 07/2026 continua aparecendo em 08/2026, assinado e travado pra edição,
|
||
// até ele encerrar ("ocultar das próximas execuções"). Por isso nada aqui
|
||
// é casado por id de linha — a linha é recriada a cada apuração, a
|
||
// observação não; o vínculo é a chave natural (`alvo_tipo`+`alvo_chave`),
|
||
// as mesmas usadas pelo reprocessamento em `_contabil_sincroniza_*`.
|
||
|
||
// Qual linha está com o painel de observações aberto, por tabela (no
|
||
// máximo uma por tabela, mesmo espírito dos antigos dcContaObsEditId/
|
||
// dcDreObsEditId/dcAvObsEditId que este objeto substituiu).
|
||
let dcObsPainelAberto = { conta: null, dre: null, analise_vertical: null };
|
||
// Observação (id) em edição inline dentro do painel — só uma por vez, e só
|
||
// enquanto ela for da competência aberta (histórica nunca é editável).
|
||
let dcObsEditandoId = null;
|
||
// "todas" | "cliente" | "internas" — filtro das listas de resumo (chips em
|
||
// cada aba e na aba "Dashboard"); não afeta a thread dentro do painel, que
|
||
// é sempre o histórico completo daquela conta.
|
||
let dcObsFiltro = "todas";
|
||
|
||
// Rótulo da aba de origem de cada tipo de alvo, usado na lista consolidada
|
||
// da aba "Dashboard" (os resumos por aba não precisam, já são de um tipo).
|
||
const PID_DC_OBS_ORIGENS = { conta: "Balancete", dre: "D.R.E.", analise_vertical: "Análise Vertical" };
|
||
|
||
// Mesma chave de ContabilObservacao.chave_conta() no backend — o `codigo`
|
||
// de classificação sozinho não é único (o Questor reaproveita o mesmo
|
||
// código pra várias contas analíticas de mesma natureza, ex. bancos
|
||
// diferentes sob "Depósitos Bancários à Vista"), então a descrição
|
||
// desambigua. Usar só `codigo` aqui fazia uma observação escrita numa
|
||
// conta aparecer em todas as outras que compartilhavam a classificação.
|
||
function dcChaveObsConta(conta) {
|
||
return `${conta.codigo}|${conta.descricao}`;
|
||
}
|
||
|
||
function dcChaveObsLinha(linha) {
|
||
return `${linha.descricao}|${linha.nivel}`;
|
||
}
|
||
|
||
function dcReindexaObservacoes() {
|
||
dcObsIndice = { conta: new Map(), dre: new Map(), analise_vertical: new Map() };
|
||
dcObservacoes.forEach((obs) => {
|
||
const mapa = dcObsIndice[obs.alvo_tipo];
|
||
if (!mapa) return;
|
||
if (!mapa.has(obs.alvo_chave)) mapa.set(obs.alvo_chave, []);
|
||
mapa.get(obs.alvo_chave).push(obs);
|
||
});
|
||
}
|
||
|
||
async function dcCarregarObservacoes() {
|
||
dcObservacoes = await pidBuscarObservacoesContabil(apuracaoAtual.id);
|
||
dcReindexaObservacoes();
|
||
}
|
||
|
||
function dcObservacoesDe(tipo, chave) {
|
||
return dcObsIndice[tipo].get(chave) || [];
|
||
}
|
||
|
||
function dcObservacoesDoTipo(tipo) {
|
||
return dcObservacoes.filter((obs) => obs.alvo_tipo === tipo);
|
||
}
|
||
|
||
function dcObsAplicaFiltro(lista) {
|
||
if (dcObsFiltro === "cliente") return lista.filter((obs) => obs.mostrar_ao_cliente);
|
||
if (dcObsFiltro === "internas") return lista.filter((obs) => !obs.mostrar_ao_cliente);
|
||
return lista;
|
||
}
|
||
|
||
function dcRenderTabelaDoTipo(tipo) {
|
||
if (tipo === "conta") renderContas();
|
||
else if (tipo === "dre") renderDre();
|
||
else renderAnaliseVertical();
|
||
}
|
||
|
||
// Qualquer mutação de observação afeta as 3 tabelas (a mesma conta pode
|
||
// aparecer em mais de uma), os resumos por aba e a lista consolidada da
|
||
// aba "Dashboard" — mais simples (e barato o bastante) re-renderizar tudo
|
||
// do que tentar acertar só o pedaço que mudou.
|
||
function dcRenderObservacoesTudo() {
|
||
renderContas();
|
||
renderDre();
|
||
renderAnaliseVertical();
|
||
renderDashboardObservacoes();
|
||
}
|
||
|
||
// Botão da coluna "Observação": ícone com a contagem de observações
|
||
// vigentes daquela conta/linha (as desta competência mais as herdadas),
|
||
// colorido quando há alguma — o texto em si nunca aparece na tabela, só no
|
||
// painel (pedido antigo do usuário pra não poluir a coluna).
|
||
//
|
||
// `temObsDescendente` (pedido explícito do usuário: "quando é feito o
|
||
// comentário em uma das contas filhas deve aparecer ressaltado o ícone na
|
||
// conta mãe também, pois caso esteja recolhida, o usuário consegue
|
||
// visualizar se há ou não observações realizadas") só importa quando a
|
||
// própria conta/linha não tem observação própria — nesse caso o ícone
|
||
// ganha um destaque mais discreto (`--descendente`, cor `--gold`, mesmo
|
||
// tom do estado "parcial" do botão "validado") em vez do `--preenchida`
|
||
// (`--accent`) usado quando a observação é da própria linha.
|
||
function dcObsBotaoHtml(tipo, alvoId, observacoes, temObsDescendente) {
|
||
const doMes = observacoes.filter((obs) => !obs.historica).length;
|
||
const herdadas = observacoes.length - doMes;
|
||
let titulo = "Adicionar observação";
|
||
if (observacoes.length) {
|
||
const partes = [];
|
||
if (doMes) partes.push(`${doMes} desta competência`);
|
||
if (herdadas) partes.push(`${herdadas} do histórico`);
|
||
titulo = `${observacoes.length} observação(ões): ${partes.join(", ")}`;
|
||
} else if (temObsDescendente) {
|
||
titulo = "Nenhuma observação nesta conta, mas há observações em contas dentro deste grupo";
|
||
}
|
||
const classeDestaque = observacoes.length
|
||
? " dc-conta-observacao-btn--preenchida"
|
||
: temObsDescendente
|
||
? " dc-conta-observacao-btn--descendente"
|
||
: "";
|
||
const contador = observacoes.length ? `<span class="dc-obs-contador">${observacoes.length}</span>` : "";
|
||
return `<button type="button" class="dc-conta-observacao-btn${classeDestaque}" data-dc-obs-abrir="${tipo}:${alvoId}" title="${pidDcEscapeHtml(titulo)}" aria-label="${pidDcEscapeHtml(titulo)}">
|
||
${PID_DC_OBSERVACAO_ICONE}${contador}
|
||
</button>`;
|
||
}
|
||
|
||
const PID_DC_ICON_ENCERRAR =
|
||
'<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="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M6 6v14h12V6"/></svg>';
|
||
const PID_DC_ICON_REATIVAR =
|
||
'<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="M3 12a9 9 0 1 0 2.64-6.36"/><path d="M3 3v6h6"/></svg>';
|
||
|
||
function dcObsItemHtml(obs, concluida) {
|
||
// "Histórica" = veio de uma competência anterior: texto/autor/data
|
||
// congelados (o backend também recusa a edição, ver
|
||
// `ContabilObservacaoViewSet._garante_texto_editavel`).
|
||
const podeEditarTexto = !concluida && !obs.historica;
|
||
const assinatura = `${pidDcEscapeHtml(obs.criado_por_nome || "Usuário removido")} · ${pidDcFormatData(obs.criado_em)} · competência ${pidDcFormatCompetencia(obs.competencia_origem)}`;
|
||
const selos = [];
|
||
if (obs.historica) selos.push('<span class="dc-obs-selo dc-obs-selo--historica">Histórico</span>');
|
||
if (obs.encerrada) {
|
||
selos.push(
|
||
`<span class="dc-obs-selo dc-obs-selo--encerrada">Encerrada em ${pidDcFormatCompetencia(obs.encerrada_em_competencia)}</span>`
|
||
);
|
||
}
|
||
if (obs.editada) selos.push('<span class="dc-obs-selo dc-obs-selo--editada">Editada</span>');
|
||
selos.push(
|
||
obs.mostrar_ao_cliente
|
||
? '<span class="dc-obs-selo dc-obs-selo--cliente">Aparece ao cliente</span>'
|
||
: '<span class="dc-obs-selo">Interna</span>'
|
||
);
|
||
|
||
const acoes = [
|
||
`<button type="button" class="icon-btn dc-dash-toggle-btn" data-dc-obs-visibilidade="${obs.id}" title="${obs.mostrar_ao_cliente ? "Ocultar do relatório do cliente" : "Mostrar no relatório do cliente"}" aria-label="${obs.mostrar_ao_cliente ? "Ocultar do relatório" : "Mostrar no relatório"}" ${concluida ? "disabled" : ""}>${obs.mostrar_ao_cliente ? PID_DC_ICON_OLHO_ABERTO : PID_DC_ICON_OLHO_FECHADO}</button>`,
|
||
];
|
||
if (obs.editada) {
|
||
acoes.push(
|
||
`<button type="button" class="icon-btn" data-dc-obs-historico="${obs.id}" title="Ver histórico de edições" aria-label="Ver histórico de edições">${PID_DC_ICON_HISTORICO}</button>`
|
||
);
|
||
}
|
||
if (podeEditarTexto) {
|
||
acoes.push(
|
||
`<button type="button" class="icon-btn" data-dc-obs-editar="${obs.id}" title="Editar esta observação" aria-label="Editar observação">${PID_DC_ICON_EDITAR}</button>`
|
||
);
|
||
}
|
||
if (!concluida) {
|
||
acoes.push(
|
||
obs.encerrada
|
||
? `<button type="button" class="icon-btn" data-dc-obs-reativar="${obs.id}" title="Voltar a exibir nas próximas competências" aria-label="Reativar observação">${PID_DC_ICON_REATIVAR}</button>`
|
||
: `<button type="button" class="icon-btn" data-dc-obs-encerrar="${obs.id}" title="Ocultar das próximas competências (continua visível nesta)" aria-label="Encerrar observação">${PID_DC_ICON_ENCERRAR}</button>`
|
||
);
|
||
}
|
||
|
||
const corpo =
|
||
dcObsEditandoId === obs.id && podeEditarTexto
|
||
? `<div class="modal-field">
|
||
<textarea id="dc-obs-edit-texto-${obs.id}" rows="3">${pidDcEscapeHtml(obs.texto)}</textarea>
|
||
</div>
|
||
<p class="modal-error" id="dc-obs-edit-erro-${obs.id}"></p>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn-outline" data-dc-obs-edit-cancelar="${obs.id}">Cancelar</button>
|
||
<button type="button" class="btn-solid" data-dc-obs-edit-salvar="${obs.id}">Salvar</button>
|
||
</div>`
|
||
: `<p class="dc-obs-item__texto">${pidDcEscapeHtml(obs.texto)}</p>`;
|
||
|
||
return `
|
||
<article class="dc-obs-item${obs.historica ? " dc-obs-item--historica" : ""}${obs.encerrada ? " dc-obs-item--encerrada" : ""}">
|
||
<header class="dc-obs-item__head">
|
||
<span class="dc-obs-item__assinatura">${assinatura}</span>
|
||
<span class="dc-obs-item__selos">${selos.join("")}</span>
|
||
<span class="dc-obs-item__acoes">${acoes.join("")}</span>
|
||
</header>
|
||
${corpo}
|
||
</article>
|
||
`;
|
||
}
|
||
|
||
// Modal "Histórico de edições" — só existe pra observações com `editada`
|
||
// (ver `ContabilObservacaoEdicao` no backend). `obs.edicoes` já vem
|
||
// carregado junto da observação (`ContabilObservacaoSerializer`), então
|
||
// não faz nenhuma chamada de API própria.
|
||
function pidDcAbrirHistoricoObservacao(obs) {
|
||
const edicoes = [...(obs.edicoes || [])].sort((a, b) => new Date(b.editado_em) - new Date(a.editado_em));
|
||
const lista = edicoes
|
||
.map(
|
||
(edicao) => `
|
||
<div class="dc-obs-historico-item">
|
||
<span class="dc-obs-historico-item__meta">${pidDcEscapeHtml(edicao.editado_por_nome || "Usuário removido")} · ${pidDcFormatData(edicao.editado_em)}</span>
|
||
<p class="dc-obs-historico-item__texto dc-obs-historico-item__texto--anterior">${pidDcEscapeHtml(edicao.texto_anterior)}</p>
|
||
<p class="dc-obs-historico-item__texto">${pidDcEscapeHtml(edicao.texto_novo)}</p>
|
||
</div>
|
||
`
|
||
)
|
||
.join("");
|
||
document.getElementById("dc-obs-historico-lista").innerHTML =
|
||
lista || '<p class="dc-obs-thread__vazio">Nenhuma edição registrada.</p>';
|
||
document.getElementById("dc-obs-historico-modal").hidden = false;
|
||
}
|
||
|
||
// `<tr>` extra logo abaixo da conta/linha (mesma `.dc-obs-edit-row` de
|
||
// antes): a thread do histórico em cima, o campo de observação nova
|
||
// embaixo. Uma observação nunca é sobrescrita — escrever de novo cria mais
|
||
// um item na thread.
|
||
function dcObsPainelHtml(tipo, alvoId, observacoes, colspan, concluida) {
|
||
const thread = observacoes.length
|
||
? observacoes.map((obs) => dcObsItemHtml(obs, concluida)).join("")
|
||
: '<p class="dc-obs-thread__vazio">Nenhuma observação registrada até agora.</p>';
|
||
const nova = concluida
|
||
? ""
|
||
: `<div class="dc-obs-nova">
|
||
<div class="modal-field">
|
||
<textarea id="dc-obs-nova-texto-${tipo}-${alvoId}" rows="3" placeholder="Escreva uma observação nova..."></textarea>
|
||
</div>
|
||
<label class="modal-checkbox">
|
||
<input type="checkbox" id="dc-obs-nova-mostrar-${tipo}-${alvoId}" />
|
||
Mostrar esta observação ao cliente no relatório
|
||
</label>
|
||
<p class="modal-error" id="dc-obs-nova-erro-${tipo}-${alvoId}"></p>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn-outline" data-dc-obs-fechar="${tipo}">Fechar</button>
|
||
<button type="button" class="btn-solid" data-dc-obs-criar="${tipo}:${alvoId}">Salvar observação</button>
|
||
</div>
|
||
</div>`;
|
||
const rodape = concluida
|
||
? '<p class="dc-obs-thread__vazio">Análise concluída, histórico somente leitura.</p>'
|
||
: "";
|
||
return `
|
||
<td colspan="${colspan}">
|
||
<div class="dc-obs-edit">
|
||
<div class="dc-obs-thread">${thread}</div>
|
||
${nova}${rodape}
|
||
</div>
|
||
</td>
|
||
`;
|
||
}
|
||
|
||
// Handler único das ações de observação, compartilhado pelos três `tbody`
|
||
// (Balancete/DRE/Análise Vertical) — devolve `true` quando tratou o clique,
|
||
// pra cada listener de tabela continuar cuidando só do que é dele
|
||
// (expandir/recolher, validado).
|
||
async function dcTrataCliqueObservacao(event, tipo) {
|
||
const abrirBtn = event.target.closest("[data-dc-obs-abrir]");
|
||
if (abrirBtn) {
|
||
const [tipoBtn, idTexto] = abrirBtn.getAttribute("data-dc-obs-abrir").split(":");
|
||
const id = Number(idTexto);
|
||
dcObsPainelAberto[tipoBtn] = dcObsPainelAberto[tipoBtn] === id ? null : id;
|
||
dcObsEditandoId = null;
|
||
dcRenderTabelaDoTipo(tipo);
|
||
return true;
|
||
}
|
||
|
||
const fecharBtn = event.target.closest("[data-dc-obs-fechar]");
|
||
if (fecharBtn) {
|
||
dcObsPainelAberto[fecharBtn.getAttribute("data-dc-obs-fechar")] = null;
|
||
dcObsEditandoId = null;
|
||
dcRenderTabelaDoTipo(tipo);
|
||
return true;
|
||
}
|
||
|
||
const criarBtn = event.target.closest("[data-dc-obs-criar]");
|
||
if (criarBtn) {
|
||
const [alvoTipo, idTexto] = criarBtn.getAttribute("data-dc-obs-criar").split(":");
|
||
const alvoId = Number(idTexto);
|
||
const erroEl = document.getElementById(`dc-obs-nova-erro-${alvoTipo}-${alvoId}`);
|
||
const texto = document.getElementById(`dc-obs-nova-texto-${alvoTipo}-${alvoId}`).value.trim();
|
||
const mostrar = document.getElementById(`dc-obs-nova-mostrar-${alvoTipo}-${alvoId}`).checked;
|
||
erroEl.textContent = "";
|
||
if (!texto) {
|
||
erroEl.textContent = "Escreva a observação antes de salvar.";
|
||
return true;
|
||
}
|
||
criarBtn.disabled = true;
|
||
try {
|
||
await pidCriarObservacaoContabil(apuracaoAtual.id, alvoTipo, alvoId, texto, mostrar);
|
||
await dcCarregarObservacoes();
|
||
dcRenderObservacoesTudo();
|
||
} catch (e) {
|
||
erroEl.textContent = e.message;
|
||
criarBtn.disabled = false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
const historicoBtn = event.target.closest("[data-dc-obs-historico]");
|
||
if (historicoBtn) {
|
||
const id = Number(historicoBtn.getAttribute("data-dc-obs-historico"));
|
||
const obs = dcObservacoes.find((o) => o.id === id);
|
||
if (obs) pidDcAbrirHistoricoObservacao(obs);
|
||
return true;
|
||
}
|
||
|
||
const editarBtn = event.target.closest("[data-dc-obs-editar]");
|
||
if (editarBtn) {
|
||
dcObsEditandoId = Number(editarBtn.getAttribute("data-dc-obs-editar"));
|
||
dcRenderTabelaDoTipo(tipo);
|
||
return true;
|
||
}
|
||
|
||
const editCancelarBtn = event.target.closest("[data-dc-obs-edit-cancelar]");
|
||
if (editCancelarBtn) {
|
||
dcObsEditandoId = null;
|
||
dcRenderTabelaDoTipo(tipo);
|
||
return true;
|
||
}
|
||
|
||
const editSalvarBtn = event.target.closest("[data-dc-obs-edit-salvar]");
|
||
if (editSalvarBtn) {
|
||
const id = Number(editSalvarBtn.getAttribute("data-dc-obs-edit-salvar"));
|
||
const erroEl = document.getElementById(`dc-obs-edit-erro-${id}`);
|
||
const texto = document.getElementById(`dc-obs-edit-texto-${id}`).value.trim();
|
||
erroEl.textContent = "";
|
||
if (!texto) {
|
||
erroEl.textContent = "A observação não pode ficar vazia. Oculte-a se não quiser mais exibi-la nas próximas competências.";
|
||
return true;
|
||
}
|
||
editSalvarBtn.disabled = true;
|
||
try {
|
||
await pidAtualizarObservacaoContabil(id, { texto });
|
||
dcObsEditandoId = null;
|
||
await dcCarregarObservacoes();
|
||
dcRenderObservacoesTudo();
|
||
} catch (e) {
|
||
erroEl.textContent = e.message;
|
||
editSalvarBtn.disabled = false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
const encerrarBtn = event.target.closest("[data-dc-obs-encerrar]");
|
||
if (encerrarBtn) {
|
||
const id = Number(encerrarBtn.getAttribute("data-dc-obs-encerrar"));
|
||
const ok = await pidConfirm(
|
||
"Ocultar esta observação das próximas competências? Ela continua registrada aqui e no histórico desta análise."
|
||
);
|
||
if (!ok) return true;
|
||
try {
|
||
await pidEncerrarObservacaoContabil(id, apuracaoAtual.id);
|
||
await dcCarregarObservacoes();
|
||
dcRenderObservacoesTudo();
|
||
} catch (e) {
|
||
await pidAlert(e.message);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
const reativarBtn = event.target.closest("[data-dc-obs-reativar]");
|
||
if (reativarBtn) {
|
||
const id = Number(reativarBtn.getAttribute("data-dc-obs-reativar"));
|
||
try {
|
||
await pidReativarObservacaoContabil(id, apuracaoAtual.id);
|
||
await dcCarregarObservacoes();
|
||
dcRenderObservacoesTudo();
|
||
} catch (e) {
|
||
await pidAlert(e.message);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
const visibilidadeBtn = event.target.closest("[data-dc-obs-visibilidade]");
|
||
if (visibilidadeBtn) {
|
||
if (visibilidadeBtn.disabled) return true;
|
||
const id = Number(visibilidadeBtn.getAttribute("data-dc-obs-visibilidade"));
|
||
const obs = dcObservacoes.find((o) => o.id === id);
|
||
visibilidadeBtn.disabled = true;
|
||
try {
|
||
await pidAtualizarObservacaoContabil(id, { mostrar_ao_cliente: !obs.mostrar_ao_cliente });
|
||
await dcCarregarObservacoes();
|
||
dcRenderObservacoesTudo();
|
||
} catch (e) {
|
||
visibilidadeBtn.disabled = false;
|
||
await pidAlert(e.message);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// Os chips "Todas / Visíveis ao cliente / Internas" existem em quatro
|
||
// lugares (resumo de cada aba + lista consolidada da aba "Dashboard") e
|
||
// compartilham a mesma variável — filtrar numa aba filtra em todas, que é o
|
||
// comportamento esperado de um filtro de visibilidade global.
|
||
document.querySelectorAll("[data-dc-obs-filtros]").forEach((grupo) => {
|
||
grupo.addEventListener("click", (event) => {
|
||
const btn = event.target.closest("[data-dc-obs-filtro]");
|
||
if (!btn) return;
|
||
dcObsFiltro = btn.getAttribute("data-dc-obs-filtro");
|
||
document.querySelectorAll("[data-dc-obs-filtro]").forEach((chip) => {
|
||
chip.classList.toggle("is-active", chip.getAttribute("data-dc-obs-filtro") === dcObsFiltro);
|
||
});
|
||
if (apuracaoAtual) dcRenderObservacoesTudo();
|
||
});
|
||
});
|
||
|
||
function renderContas() {
|
||
const body = document.getElementById("dc-contas-body");
|
||
body.innerHTML = "";
|
||
const concluida = apuracaoAtual.status === "concluida";
|
||
const contas = apuracaoAtual.contas;
|
||
const niveis = contas.map(dcContaNivel);
|
||
const temFilhos = contas.map((_, i) => i + 1 < contas.length && niveis[i + 1] > niveis[i]);
|
||
|
||
// Pilha dos níveis de ancestrais recolhidos — enquanto uma linha tiver
|
||
// algum ancestral na pilha, ela fica oculta (é descendente de um grupo
|
||
// fechado), mesmo padrão de renderização de árvore a partir de uma
|
||
// lista plana com profundidade já conhecida.
|
||
const pilhaColapso = [];
|
||
contas.forEach((conta, i) => {
|
||
const nivel = niveis[i];
|
||
while (pilhaColapso.length && pilhaColapso[pilhaColapso.length - 1] >= nivel) {
|
||
pilhaColapso.pop();
|
||
}
|
||
const oculta = pilhaColapso.length > 0;
|
||
if (temFilhos[i] && dcContasColapsadas.has(conta.id)) {
|
||
pilhaColapso.push(nivel);
|
||
}
|
||
if (oculta) return;
|
||
|
||
const expandida = !dcContasColapsadas.has(conta.id);
|
||
const toggle = temFilhos[i]
|
||
? `<button type="button" class="dc-conta-toggle${expandida ? " is-expanded" : ""}" data-dc-conta-toggle="${conta.id}" aria-label="${expandida ? "Recolher" : "Expandir"} ${pidDcEscapeHtml(conta.descricao)}">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||
</button>`
|
||
: `<span class="dc-conta-toggle-spacer"></span>`;
|
||
|
||
const validadoInfo = dcValidadoInfo(conta, contas, dcContaNivel, temFilhos[i]);
|
||
const observacoesDaConta = dcObservacoesDe("conta", dcChaveObsConta(conta));
|
||
const temObsDescendente =
|
||
temFilhos[i] && dcTemObservacaoDescendente("conta", contas, dcContaNivel, dcChaveObsConta, conta.id);
|
||
|
||
const tr = document.createElement("tr");
|
||
if (conta.tipo === "S") tr.classList.add("dc-conta-row--sintetica");
|
||
if (dcContasDestaque.has(conta.id)) tr.classList.add("dc-conta-row--destaque");
|
||
tr.innerHTML = `
|
||
<td>${pidDcEscapeHtml(conta.conta_numero)}</td>
|
||
<td>${pidDcEscapeHtml(conta.codigo)}</td>
|
||
<td style="padding-left: calc(var(--space-4) + ${nivel * 18}px)">
|
||
<span class="dc-conta-desc-cell">${toggle}<span>${pidDcEscapeHtml(conta.descricao)}</span></span>
|
||
</td>
|
||
<td>${pidDcFormatMoeda(conta.saldo_anterior)}</td>
|
||
<td>${pidDcFormatMoeda(conta.debito)}</td>
|
||
<td>${pidDcFormatMoeda(conta.credito)}</td>
|
||
<td>${pidDcFormatMoeda(conta.saldo_atual)}</td>
|
||
<td>
|
||
<div class="dc-obs-cell-actions">
|
||
<button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-conta-validado="${conta.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}>
|
||
${PID_DC_VALIDADO_ICONE}
|
||
</button>
|
||
${dcObsBotaoHtml("conta", conta.id, observacoesDaConta, temObsDescendente)}
|
||
${pidDcAlteradaBadgeHtml(
|
||
conta.alterada_reprocessamento,
|
||
conta.validado,
|
||
conta.valor_anterior_reprocessamento != null ? pidDcFormatMoeda(conta.valor_anterior_reprocessamento) : null
|
||
)}
|
||
</div>
|
||
</td>
|
||
`;
|
||
body.appendChild(tr);
|
||
|
||
if (dcObsPainelAberto.conta === conta.id) {
|
||
const editRow = document.createElement("tr");
|
||
editRow.className = "dc-obs-edit-row";
|
||
editRow.innerHTML = dcObsPainelHtml("conta", conta.id, observacoesDaConta, 8, concluida);
|
||
body.appendChild(editRow);
|
||
}
|
||
});
|
||
|
||
renderContasObsResumo();
|
||
}
|
||
|
||
document.getElementById("dc-contas-body").addEventListener("click", async (event) => {
|
||
const toggleBtn = event.target.closest("[data-dc-conta-toggle]");
|
||
if (toggleBtn) {
|
||
const id = Number(toggleBtn.getAttribute("data-dc-conta-toggle"));
|
||
const estavaColapsada = dcContasColapsadas.has(id);
|
||
if (estavaColapsada) dcContasColapsadas.delete(id);
|
||
else dcContasColapsadas.add(id);
|
||
// Expandir empilha o destaque atual e troca pros filhos recém-revelados;
|
||
// recolher desempilha e restaura o destaque de antes — ver dcContasDestaque acima.
|
||
if (estavaColapsada) {
|
||
dcContasDestaqueHistorico.push(dcContasDestaque);
|
||
dcContasDestaque = dcFilhosDiretos(apuracaoAtual.contas, dcContaNivel, id);
|
||
} else {
|
||
dcContasDestaque = dcContasDestaqueHistorico.pop() || new Set();
|
||
}
|
||
renderContas();
|
||
return;
|
||
}
|
||
|
||
const validadoBtn = event.target.closest("[data-dc-conta-validado]");
|
||
if (validadoBtn) {
|
||
if (validadoBtn.disabled) return;
|
||
const id = Number(validadoBtn.getAttribute("data-dc-conta-validado"));
|
||
validadoBtn.disabled = true;
|
||
try {
|
||
await dcClicarValidadoConta(id);
|
||
} catch (e) {
|
||
await pidAlert(e.message);
|
||
} finally {
|
||
validadoBtn.disabled = false;
|
||
}
|
||
return;
|
||
}
|
||
|
||
await dcTrataCliqueObservacao(event, "conta");
|
||
});
|
||
|
||
// Mesma árvore recolhível do Balancete (ver renderContas), agora sobre
|
||
// ContabilLinhaDre.nivel — que já chega pronto do backend na ordem de
|
||
// leitura do PDF, então "ter filhos" é só olhar se a próxima linha tem
|
||
// nível maior que o da linha atual.
|
||
function renderDre() {
|
||
const body = document.getElementById("dc-dre-body");
|
||
body.innerHTML = "";
|
||
const concluida = apuracaoAtual.status === "concluida";
|
||
const linhas = apuracaoAtual.linhas_dre;
|
||
const niveis = linhas.map((linha) => Math.max(0, linha.nivel));
|
||
const temFilhos = linhas.map((_, i) => i + 1 < linhas.length && niveis[i + 1] > niveis[i]);
|
||
|
||
const pilhaColapso = [];
|
||
linhas.forEach((linha, i) => {
|
||
const nivel = niveis[i];
|
||
while (pilhaColapso.length && pilhaColapso[pilhaColapso.length - 1] >= nivel) {
|
||
pilhaColapso.pop();
|
||
}
|
||
const oculta = pilhaColapso.length > 0;
|
||
if (temFilhos[i] && dcDreColapsadas.has(linha.id)) {
|
||
pilhaColapso.push(nivel);
|
||
}
|
||
if (oculta) return;
|
||
|
||
const expandida = !dcDreColapsadas.has(linha.id);
|
||
const toggle = temFilhos[i]
|
||
? `<button type="button" class="dc-conta-toggle${expandida ? " is-expanded" : ""}" data-dc-dre-toggle="${linha.id}" aria-label="${expandida ? "Recolher" : "Expandir"} ${pidDcEscapeHtml(linha.descricao)}">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||
</button>`
|
||
: `<span class="dc-conta-toggle-spacer"></span>`;
|
||
|
||
const validadoInfo = dcValidadoInfo(linha, linhas, (l) => Math.max(0, l.nivel), temFilhos[i]);
|
||
const observacoesDaLinha = dcObservacoesDe("dre", dcChaveObsLinha(linha));
|
||
const temObsDescendente =
|
||
temFilhos[i] &&
|
||
dcTemObservacaoDescendente("dre", linhas, (l) => Math.max(0, l.nivel), dcChaveObsLinha, linha.id);
|
||
|
||
const tr = document.createElement("tr");
|
||
tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row";
|
||
if (dcDreDestaque.has(linha.id)) tr.classList.add("dc-conta-row--destaque");
|
||
const indentacao = nivel * 16;
|
||
tr.innerHTML = `
|
||
<td style="padding-left: calc(var(--space-4) + ${indentacao}px)">
|
||
<span class="dc-conta-desc-cell">${toggle}<span>${pidDcEscapeHtml(linha.descricao)}</span></span>
|
||
</td>
|
||
<td>${pidDcFormatMoeda(linha.valor)}</td>
|
||
<td>
|
||
<div class="dc-obs-cell-actions">
|
||
<button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-dre-validado="${linha.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}>
|
||
${PID_DC_VALIDADO_ICONE}
|
||
</button>
|
||
${dcObsBotaoHtml("dre", linha.id, observacoesDaLinha, temObsDescendente)}
|
||
${pidDcAlteradaBadgeHtml(
|
||
linha.alterada_reprocessamento,
|
||
linha.validado,
|
||
linha.valor_anterior_reprocessamento != null ? pidDcFormatMoeda(linha.valor_anterior_reprocessamento) : null
|
||
)}
|
||
</div>
|
||
</td>
|
||
`;
|
||
body.appendChild(tr);
|
||
|
||
if (dcObsPainelAberto.dre === linha.id) {
|
||
const editRow = document.createElement("tr");
|
||
editRow.className = "dc-obs-edit-row";
|
||
editRow.innerHTML = dcObsPainelHtml("dre", linha.id, observacoesDaLinha, 3, concluida);
|
||
body.appendChild(editRow);
|
||
}
|
||
});
|
||
|
||
renderDreObsResumo();
|
||
}
|
||
|
||
document.getElementById("dc-dre-body").addEventListener("click", async (event) => {
|
||
const toggleBtn = event.target.closest("[data-dc-dre-toggle]");
|
||
if (toggleBtn) {
|
||
const id = Number(toggleBtn.getAttribute("data-dc-dre-toggle"));
|
||
const estavaColapsada = dcDreColapsadas.has(id);
|
||
if (estavaColapsada) dcDreColapsadas.delete(id);
|
||
else dcDreColapsadas.add(id);
|
||
const nivelFn = (linha) => Math.max(0, linha.nivel);
|
||
if (estavaColapsada) {
|
||
dcDreDestaqueHistorico.push(dcDreDestaque);
|
||
dcDreDestaque = dcFilhosDiretos(apuracaoAtual.linhas_dre, nivelFn, id);
|
||
} else {
|
||
dcDreDestaque = dcDreDestaqueHistorico.pop() || new Set();
|
||
}
|
||
renderDre();
|
||
return;
|
||
}
|
||
|
||
const validadoBtn = event.target.closest("[data-dc-dre-validado]");
|
||
if (validadoBtn) {
|
||
if (validadoBtn.disabled) return;
|
||
const id = Number(validadoBtn.getAttribute("data-dc-dre-validado"));
|
||
validadoBtn.disabled = true;
|
||
try {
|
||
await dcClicarValidadoLinha(id);
|
||
} catch (e) {
|
||
await pidAlert(e.message);
|
||
} finally {
|
||
validadoBtn.disabled = false;
|
||
}
|
||
return;
|
||
}
|
||
|
||
await dcTrataCliqueObservacao(event, "dre");
|
||
});
|
||
|
||
// Cabeçalho de coluna da Análise Vertical é montado em JS (não hardcoded
|
||
// no HTML) porque o número de meses varia conforme o período do PDF
|
||
// (normalmente 3, ver ContabilApuracao.analise_vertical_meses) — cada mês
|
||
// vira duas colunas (Valor/Variação).
|
||
function renderAnaliseVerticalHead() {
|
||
const meses = apuracaoAtual.analise_vertical_meses || [];
|
||
const row = document.getElementById("dc-av-head-row");
|
||
row.innerHTML = `
|
||
<th>Descrição</th>
|
||
${meses
|
||
.map((mes) => {
|
||
const label = pidDcEscapeHtml(pidDcFormatMesAnaliseVertical(mes));
|
||
return `<th>${label} — Valor</th><th>${label} — Variação</th>`;
|
||
})
|
||
.join("")}
|
||
<th>
|
||
<span class="dc-th-linha">Observação
|
||
<button type="button" class="icon-btn dc-reset-formatacao-btn" data-dc-reset-formatacao="analise-vertical" aria-label="Restaurar formatação padrão da Análise Vertical" title="Restaurar formatação padrão (recolher grupos como no início)">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 2.64-6.36"/><path d="M3 3v6h6"/></svg>
|
||
</button>
|
||
</span>
|
||
</th>
|
||
`;
|
||
}
|
||
|
||
// Mesma árvore recolhível do Balancete/DRE (ver renderContas/renderDre),
|
||
// agora sobre ContabilLinhaAnaliseVertical — cada linha mostra um
|
||
// valor+percentual por mês (colunas dinâmicas) em vez de um valor único.
|
||
function renderAnaliseVertical() {
|
||
renderAnaliseVerticalHead();
|
||
const body = document.getElementById("dc-av-body");
|
||
body.innerHTML = "";
|
||
const concluida = apuracaoAtual.status === "concluida";
|
||
const linhas = apuracaoAtual.linhas_analise_vertical;
|
||
const colspan = 2 + (apuracaoAtual.analise_vertical_meses || []).length * 2;
|
||
const nivelFn = (l) => Math.max(0, l.nivel);
|
||
const niveis = linhas.map(nivelFn);
|
||
const temFilhos = linhas.map((_, i) => i + 1 < linhas.length && niveis[i + 1] > niveis[i]);
|
||
|
||
const pilhaColapso = [];
|
||
linhas.forEach((linha, i) => {
|
||
const nivel = niveis[i];
|
||
while (pilhaColapso.length && pilhaColapso[pilhaColapso.length - 1] >= nivel) {
|
||
pilhaColapso.pop();
|
||
}
|
||
const oculta = pilhaColapso.length > 0;
|
||
if (temFilhos[i] && dcAvColapsadas.has(linha.id)) {
|
||
pilhaColapso.push(nivel);
|
||
}
|
||
if (oculta) return;
|
||
|
||
const expandida = !dcAvColapsadas.has(linha.id);
|
||
const toggle = temFilhos[i]
|
||
? `<button type="button" class="dc-conta-toggle${expandida ? " is-expanded" : ""}" data-dc-av-toggle="${linha.id}" aria-label="${expandida ? "Recolher" : "Expandir"} ${pidDcEscapeHtml(linha.descricao)}">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||
</button>`
|
||
: `<span class="dc-conta-toggle-spacer"></span>`;
|
||
|
||
const validadoInfo = dcValidadoInfo(linha, linhas, nivelFn, temFilhos[i]);
|
||
const observacoesDaLinha = dcObservacoesDe("analise_vertical", dcChaveObsLinha(linha));
|
||
const temObsDescendente =
|
||
temFilhos[i] && dcTemObservacaoDescendente("analise_vertical", linhas, nivelFn, dcChaveObsLinha, linha.id);
|
||
const colunasMensais = linha.valores
|
||
.map(
|
||
(v) =>
|
||
`<td>${pidDcFormatMoeda(v.valor)}</td><td>${pidDcFormatPercentualAnaliseVertical(v.percentual)}</td>`
|
||
)
|
||
.join("");
|
||
|
||
const tr = document.createElement("tr");
|
||
tr.className = linha.totalizador ? "dc-dre-row dc-dre-row--totalizador" : "dc-dre-row";
|
||
if (dcAvDestaque.has(linha.id)) tr.classList.add("dc-conta-row--destaque");
|
||
const indentacao = nivel * 16;
|
||
tr.innerHTML = `
|
||
<td style="padding-left: calc(var(--space-4) + ${indentacao}px)">
|
||
<span class="dc-conta-desc-cell">${toggle}<span>${pidDcEscapeHtml(linha.descricao)}</span></span>
|
||
</td>
|
||
${colunasMensais}
|
||
<td>
|
||
<div class="dc-obs-cell-actions">
|
||
<button type="button" class="dc-conta-validado-btn${validadoInfo.classe}" data-dc-av-validado="${linha.id}" title="${validadoInfo.title}" aria-label="${validadoInfo.label}" aria-pressed="${validadoInfo.pressed}" ${concluida ? "disabled" : ""}>
|
||
${PID_DC_VALIDADO_ICONE}
|
||
</button>
|
||
${dcObsBotaoHtml("analise_vertical", linha.id, observacoesDaLinha, temObsDescendente)}
|
||
${pidDcAlteradaBadgeHtml(linha.alterada_reprocessamento, linha.validado, pidDcValorAnteriorAvTexto(linha))}
|
||
</div>
|
||
</td>
|
||
`;
|
||
body.appendChild(tr);
|
||
|
||
if (dcObsPainelAberto.analise_vertical === linha.id) {
|
||
const editRow = document.createElement("tr");
|
||
editRow.className = "dc-obs-edit-row";
|
||
editRow.innerHTML = dcObsPainelHtml("analise_vertical", linha.id, observacoesDaLinha, colspan, concluida);
|
||
body.appendChild(editRow);
|
||
}
|
||
});
|
||
|
||
renderAnaliseVerticalObsResumo();
|
||
}
|
||
|
||
document.getElementById("dc-av-body").addEventListener("click", async (event) => {
|
||
const toggleBtn = event.target.closest("[data-dc-av-toggle]");
|
||
if (toggleBtn) {
|
||
const id = Number(toggleBtn.getAttribute("data-dc-av-toggle"));
|
||
const estavaColapsada = dcAvColapsadas.has(id);
|
||
if (estavaColapsada) dcAvColapsadas.delete(id);
|
||
else dcAvColapsadas.add(id);
|
||
const nivelFn = (linha) => Math.max(0, linha.nivel);
|
||
if (estavaColapsada) {
|
||
dcAvDestaqueHistorico.push(dcAvDestaque);
|
||
dcAvDestaque = dcFilhosDiretos(apuracaoAtual.linhas_analise_vertical, nivelFn, id);
|
||
} else {
|
||
dcAvDestaque = dcAvDestaqueHistorico.pop() || new Set();
|
||
}
|
||
renderAnaliseVertical();
|
||
return;
|
||
}
|
||
|
||
const validadoBtn = event.target.closest("[data-dc-av-validado]");
|
||
if (validadoBtn) {
|
||
if (validadoBtn.disabled) return;
|
||
const id = Number(validadoBtn.getAttribute("data-dc-av-validado"));
|
||
validadoBtn.disabled = true;
|
||
try {
|
||
await dcClicarValidadoAv(id);
|
||
} catch (e) {
|
||
await pidAlert(e.message);
|
||
} finally {
|
||
validadoBtn.disabled = false;
|
||
}
|
||
return;
|
||
}
|
||
|
||
await dcTrataCliqueObservacao(event, "analise_vertical");
|
||
});
|
||
|
||
// ---- 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();
|
||
});
|
||
|
||
// ---- Restaurar formatação padrão (Balancete/DRE/Análise Vertical) ----
|
||
// Botão no canto direito da linha de cabeçalho de cada tabela — descarta
|
||
// todo expandir/recolher manual feito pelo contador e recalcula o mesmo
|
||
// estado inicial de dcColapsoPadrao()/dcUltimaLevaVisivel() usado quando a
|
||
// apuração é aberta (ver renderRevisao). Cada árvore (Balancete/DRE/Análise
|
||
// Vertical) tem seu próprio botão porque cada uma tem seu próprio estado de
|
||
// colapso/destaque, independente das demais.
|
||
document.getElementById("dc-contas-thead").addEventListener("click", (event) => {
|
||
if (!event.target.closest("[data-dc-reset-formatacao]")) return;
|
||
dcContasColapsadas = dcColapsoPadrao(apuracaoAtual.contas, dcContaNivel);
|
||
dcContasDestaque = dcUltimaLevaVisivel(apuracaoAtual.contas, dcContaNivel, dcContasColapsadas);
|
||
dcContasDestaqueHistorico = [];
|
||
renderContas();
|
||
});
|
||
|
||
document.getElementById("dc-dre-thead").addEventListener("click", (event) => {
|
||
if (!event.target.closest("[data-dc-reset-formatacao]")) return;
|
||
const nivelFn = (linha) => Math.max(0, linha.nivel);
|
||
dcDreColapsadas = dcColapsoPadrao(apuracaoAtual.linhas_dre, nivelFn);
|
||
dcDreDestaque = dcUltimaLevaVisivel(apuracaoAtual.linhas_dre, nivelFn, dcDreColapsadas);
|
||
dcDreDestaqueHistorico = [];
|
||
renderDre();
|
||
});
|
||
|
||
document.getElementById("dc-av-thead").addEventListener("click", (event) => {
|
||
if (!event.target.closest("[data-dc-reset-formatacao]")) return;
|
||
const nivelFn = (linha) => Math.max(0, linha.nivel);
|
||
dcAvColapsadas = dcColapsoPadrao(apuracaoAtual.linhas_analise_vertical, nivelFn);
|
||
dcAvDestaque = dcUltimaLevaVisivel(apuracaoAtual.linhas_analise_vertical, nivelFn, dcAvColapsadas);
|
||
dcAvDestaqueHistorico = [];
|
||
renderAnaliseVertical();
|
||
});
|
||
|
||
// ---- Resumo do Fechamento (texto rico, aba "Dashboard") ----
|
||
// Editor contenteditable + colar/arrastar imagem embutida — mesmo padrão
|
||
// de `.ag-richtext` (acessos-gerais.js) e `.ajuda-modal__editor`
|
||
// (ajuda-aplicacao.js), duplicado aqui de propósito (não é um componente
|
||
// compartilhado no projeto). Populado só em `renderRevisao()` (uma vez
|
||
// por apuração aberta), nunca em `renderDashboardTab()` — que roda toda
|
||
// vez que o contador troca de aba, e resetar o `innerHTML` a cada troca
|
||
// descartaria texto ainda não salvo.
|
||
|
||
const PID_DC_RESUMO_FECHAMENTO_IMAGE_MAX_BYTES = 2 * 1024 * 1024;
|
||
const resumoFechamentoEditor = document.getElementById("dc-dash-resumo-fechamento-editor");
|
||
const resumoFechamentoSalvarBtn = document.getElementById("dc-dash-resumo-fechamento-salvar-btn");
|
||
const resumoFechamentoStatusEl = document.getElementById("dc-dash-resumo-fechamento-status");
|
||
|
||
function renderResumoFechamento() {
|
||
const concluida = apuracaoAtual.status === "concluida";
|
||
resumoFechamentoEditor.innerHTML = apuracaoAtual.resumo_fechamento || "";
|
||
resumoFechamentoEditor.setAttribute("contenteditable", concluida ? "false" : "true");
|
||
resumoFechamentoSalvarBtn.hidden = concluida;
|
||
resumoFechamentoStatusEl.hidden = true;
|
||
}
|
||
|
||
function pidDcResumoFechamentoPlaceCaretAtPoint(x, y) {
|
||
const selection = window.getSelection();
|
||
if (document.caretRangeFromPoint) {
|
||
const range = document.caretRangeFromPoint(x, y);
|
||
if (range) {
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
}
|
||
} else if (document.caretPositionFromPoint) {
|
||
const pos = document.caretPositionFromPoint(x, y);
|
||
if (pos) {
|
||
const range = document.createRange();
|
||
range.setStart(pos.offsetNode, pos.offset);
|
||
range.collapse(true);
|
||
selection.removeAllRanges();
|
||
selection.addRange(range);
|
||
}
|
||
}
|
||
}
|
||
|
||
function pidDcResumoFechamentoMostraStatus(texto) {
|
||
resumoFechamentoStatusEl.hidden = false;
|
||
resumoFechamentoStatusEl.textContent = texto;
|
||
}
|
||
|
||
function pidDcResumoFechamentoInserirImagem(file, atPoint) {
|
||
if (!file || !file.type.startsWith("image/")) return;
|
||
if (file.size > PID_DC_RESUMO_FECHAMENTO_IMAGE_MAX_BYTES) {
|
||
pidDcResumoFechamentoMostraStatus("A imagem deve ter no máximo 2MB.");
|
||
return;
|
||
}
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
resumoFechamentoEditor.focus();
|
||
if (atPoint) pidDcResumoFechamentoPlaceCaretAtPoint(atPoint.x, atPoint.y);
|
||
document.execCommand("insertImage", false, reader.result);
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
resumoFechamentoEditor.addEventListener("paste", (event) => {
|
||
const items = Array.from((event.clipboardData && event.clipboardData.items) || []);
|
||
const imageItem = items.find((item) => item.type.startsWith("image/"));
|
||
if (!imageItem) return;
|
||
event.preventDefault();
|
||
pidDcResumoFechamentoInserirImagem(imageItem.getAsFile());
|
||
});
|
||
|
||
resumoFechamentoEditor.addEventListener("dragover", (event) => {
|
||
event.preventDefault();
|
||
});
|
||
|
||
resumoFechamentoEditor.addEventListener("drop", (event) => {
|
||
const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
|
||
if (!file || !file.type.startsWith("image/")) return;
|
||
event.preventDefault();
|
||
pidDcResumoFechamentoInserirImagem(file, { x: event.clientX, y: event.clientY });
|
||
});
|
||
|
||
resumoFechamentoSalvarBtn.addEventListener("click", async () => {
|
||
const hasImage = resumoFechamentoEditor.querySelector("img") !== null;
|
||
const hasText = resumoFechamentoEditor.textContent.trim() !== "";
|
||
const valor = hasImage || hasText ? resumoFechamentoEditor.innerHTML : "";
|
||
resumoFechamentoSalvarBtn.disabled = true;
|
||
try {
|
||
const resultado = await pidAtualizarResumoFechamentoContabil(apuracaoAtual.id, valor);
|
||
apuracaoAtual.resumo_fechamento = resultado.resumo_fechamento;
|
||
pidDcResumoFechamentoMostraStatus("Salvo.");
|
||
} catch (e) {
|
||
pidDcResumoFechamentoMostraStatus(e.message);
|
||
} finally {
|
||
resumoFechamentoSalvarBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
// ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ----
|
||
|
||
// Indicadores só são buscados na primeira vez que a aba é aberta (mesmo
|
||
// espírito de "canvas com tamanho zero não desenha certo" do relatório em
|
||
// si — aqui o motivo é só evitar um cálculo/consulta extra ao banco em
|
||
// toda apuração aberta, já que boa parte das revisões nem chega a olhar
|
||
// esta aba). Observações (contas/DRE/achados) já estão todas carregadas
|
||
// em apuracaoAtual, não precisam de fetch próprio.
|
||
async function renderDashboardTab() {
|
||
if (!dcIndicadoresAtual) {
|
||
dcIndicadoresAtual = await pidBuscarIndicadoresContabil(apuracaoAtual.id);
|
||
}
|
||
renderDashboardIndicadores();
|
||
renderDashboardObservacoes();
|
||
}
|
||
|
||
// Agrupa as chaves de `metadados` pelo campo `grupo` de cada uma (vem do
|
||
// servidor — os 11 de sistema já têm um grupo fixo, personalizados
|
||
// sempre caem em "Indicadores Personalizados", ver
|
||
// `ContabilApuracaoViewSet.indicadores()`); só a ORDEM de exibição dos
|
||
// grupos é decidida aqui (`PID_DC_DASH_ORDEM_GRUPOS`).
|
||
function renderDashboardIndicadores() {
|
||
const container = document.getElementById("dc-dash-indicadores");
|
||
container.innerHTML = "";
|
||
if (!dcIndicadoresAtual) return;
|
||
const { indicadores, indicadores_ocultos: indicadoresOcultos, metadados } = dcIndicadoresAtual;
|
||
const concluida = apuracaoAtual.status === "concluida";
|
||
|
||
const grupos = new Map();
|
||
Object.keys(metadados).forEach((chave) => {
|
||
const nomeGrupo = metadados[chave].grupo;
|
||
if (!grupos.has(nomeGrupo)) grupos.set(nomeGrupo, []);
|
||
grupos.get(nomeGrupo).push(chave);
|
||
});
|
||
const nomesGrupos = [...grupos.keys()].sort((a, b) => {
|
||
const posicaoA = PID_DC_DASH_ORDEM_GRUPOS.indexOf(a);
|
||
const posicaoB = PID_DC_DASH_ORDEM_GRUPOS.indexOf(b);
|
||
return (posicaoA === -1 ? 99 : posicaoA) - (posicaoB === -1 ? 99 : posicaoB);
|
||
});
|
||
|
||
nomesGrupos.forEach((nomeGrupo) => {
|
||
const secao = document.createElement("div");
|
||
secao.className = "dc-dash-grupo";
|
||
secao.innerHTML = `
|
||
<h4 class="dc-dash-grupo__titulo">${pidDcEscapeHtml(nomeGrupo)}</h4>
|
||
<div class="dc-dash-cards-grid"></div>
|
||
`;
|
||
const grid = secao.querySelector(".dc-dash-cards-grid");
|
||
grupos.get(nomeGrupo).forEach((chave) => {
|
||
const meta = metadados[chave];
|
||
const oculto = indicadoresOcultos.includes(chave);
|
||
const card = document.createElement("div");
|
||
card.className = `dc-dash-card${oculto ? " is-oculto" : ""}`;
|
||
card.innerHTML = `
|
||
<div class="dc-dash-card__info">
|
||
<span class="dc-dash-card__label">${pidDcEscapeHtml(meta.nome)}</span>
|
||
<span class="dc-dash-card__valor">${pidDcFormatIndicadorGenerico(meta.formato, indicadores[chave])}</span>
|
||
</div>
|
||
<div class="dc-dash-card__actions">
|
||
<button
|
||
type="button"
|
||
class="icon-btn dc-dash-toggle-btn"
|
||
data-dc-dash-indicador-editar="${meta.definicao_id}"
|
||
aria-label="Editar indicador"
|
||
title="Editar indicador"
|
||
${concluida ? "disabled" : ""}
|
||
>${PID_DC_ICON_EDITAR}</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn dc-dash-toggle-btn"
|
||
data-dc-dash-indicador-toggle="${chave}"
|
||
aria-label="${oculto ? "Mostrar" : "Ocultar"} no relatório"
|
||
title="${oculto ? "Mostrar no relatório" : "Ocultar do relatório"}"
|
||
${concluida ? "disabled" : ""}
|
||
>${oculto ? PID_DC_ICON_OLHO_FECHADO : PID_DC_ICON_OLHO_ABERTO}</button>
|
||
</div>
|
||
`;
|
||
card.addEventListener("click", (event) => {
|
||
if (event.target.closest("button")) return;
|
||
pidDcAbrirFormulaModal(meta);
|
||
});
|
||
grid.appendChild(card);
|
||
});
|
||
container.appendChild(secao);
|
||
});
|
||
}
|
||
|
||
document.getElementById("dc-dash-indicadores").addEventListener("click", async (event) => {
|
||
const editarBtn = event.target.closest("[data-dc-dash-indicador-editar]");
|
||
if (editarBtn) {
|
||
if (editarBtn.disabled) return;
|
||
const definicaoId = Number(editarBtn.getAttribute("data-dc-dash-indicador-editar"));
|
||
try {
|
||
const definicao = await pidBuscarIndicadorContabilDefinicao(definicaoId);
|
||
pidDcAbrirIndicadorModal(definicao);
|
||
} catch (e) {
|
||
await pidAlert(e.message);
|
||
}
|
||
return;
|
||
}
|
||
const btn = event.target.closest("[data-dc-dash-indicador-toggle]");
|
||
if (!btn || btn.disabled) return;
|
||
const chave = btn.getAttribute("data-dc-dash-indicador-toggle");
|
||
const atual = dcIndicadoresAtual.indicadores_ocultos;
|
||
const novaLista = atual.includes(chave) ? atual.filter((c) => c !== chave) : [...atual, chave];
|
||
btn.disabled = true;
|
||
try {
|
||
const resposta = await pidAtualizarIndicadoresOcultosContabil(apuracaoAtual.id, novaLista);
|
||
dcIndicadoresAtual.indicadores_ocultos = resposta.indicadores_ocultos;
|
||
renderDashboardIndicadores();
|
||
} catch (e) {
|
||
btn.disabled = false;
|
||
await pidAlert(e.message);
|
||
}
|
||
});
|
||
|
||
// ---- Modal "Ver fórmula" ----
|
||
|
||
// Aceita um objeto {nome, descricao, formula_texto} direto — não só uma
|
||
// chave de `dcIndicadoresAtual.metadados` (que só lista indicadores
|
||
// ATIVOS nesta apuração). Um indicador não padrão ainda não selecionado
|
||
// pra esta apuração não aparece em `metadados`, mas o hub de
|
||
// "Gerenciar Indicadores" (ver pidDcCriaLinhaHub) ainda precisa poder
|
||
// mostrar a fórmula dele — por isso o chamador monta o objeto certo em
|
||
// cada caso, em vez desta função sempre resolver por chave.
|
||
function pidDcAbrirFormulaModal(meta) {
|
||
if (!meta) return;
|
||
document.getElementById("dc-indicador-formula-modal-titulo").textContent = meta.nome;
|
||
document.getElementById("dc-indicador-formula-modal-descricao").textContent =
|
||
meta.descricao || "Sem descrição cadastrada.";
|
||
document.getElementById("dc-indicador-formula-modal-formula").textContent = meta.formula_texto || "—";
|
||
document.getElementById("dc-indicador-formula-modal").hidden = false;
|
||
}
|
||
|
||
document.getElementById("dc-indicador-formula-modal-fechar-btn").addEventListener("click", () => {
|
||
document.getElementById("dc-indicador-formula-modal").hidden = true;
|
||
});
|
||
document.getElementById("dc-indicador-formula-modal").addEventListener("click", (event) => {
|
||
if (event.target.id === "dc-indicador-formula-modal") {
|
||
document.getElementById("dc-indicador-formula-modal").hidden = true;
|
||
}
|
||
});
|
||
|
||
document.getElementById("dc-obs-historico-modal-fechar-btn").addEventListener("click", () => {
|
||
document.getElementById("dc-obs-historico-modal").hidden = true;
|
||
});
|
||
document.getElementById("dc-obs-historico-modal").addEventListener("click", (event) => {
|
||
if (event.target.id === "dc-obs-historico-modal") {
|
||
document.getElementById("dc-obs-historico-modal").hidden = true;
|
||
}
|
||
});
|
||
|
||
// ---- Modal "Novo/Editar Indicador" ----
|
||
|
||
// Opções de ícone do card no relatório "Gerar Dashboard" — cópia irmã de
|
||
// `_CONTABIL_ICONES_SVG` em portal_api/views.py (mesmas chaves/desenhos),
|
||
// mantida em sincronia manualmente (ver comentário em
|
||
// IndicadorContabilDefinicao.icone, models.py, sobre por que não dá pra
|
||
// ter uma fonte única entre o backend e este JS).
|
||
const PID_DC_INDICADOR_ICONES = [
|
||
{ chave: "barras", rotulo: "Barras", svg: '<path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/>' },
|
||
{
|
||
chave: "tendencia-alta",
|
||
rotulo: "Tendência de alta",
|
||
svg: '<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>',
|
||
},
|
||
{
|
||
chave: "tendencia-baixa",
|
||
rotulo: "Tendência de baixa",
|
||
svg: '<polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/><polyline points="17 18 23 18 23 12"/>',
|
||
},
|
||
{
|
||
chave: "percentual",
|
||
rotulo: "Percentual",
|
||
svg: '<line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/>',
|
||
},
|
||
{
|
||
chave: "pizza",
|
||
rotulo: "Gráfico de pizza",
|
||
svg: '<path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/>',
|
||
},
|
||
{ chave: "atividade", rotulo: "Atividade", svg: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>' },
|
||
{
|
||
chave: "moeda",
|
||
rotulo: "Cifrão",
|
||
svg: '<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>',
|
||
},
|
||
{
|
||
chave: "alvo",
|
||
rotulo: "Alvo/meta",
|
||
svg: '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
|
||
},
|
||
{
|
||
chave: "camadas",
|
||
rotulo: "Camadas",
|
||
svg:
|
||
'<polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/>' +
|
||
'<polyline points="2 12 12 17 22 12"/>',
|
||
},
|
||
{
|
||
chave: "cartao",
|
||
rotulo: "Cartão",
|
||
svg: '<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>',
|
||
},
|
||
{
|
||
chave: "selo",
|
||
rotulo: "Selo/prêmio",
|
||
svg: '<circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/>',
|
||
},
|
||
];
|
||
const PID_DC_INDICADOR_ICONE_PADRAO = "barras";
|
||
|
||
const indicadorModal = document.getElementById("dc-indicador-modal");
|
||
const indicadorComponentesEl = document.getElementById("dc-indicador-componentes");
|
||
const indicadorIconesEl = document.getElementById("dc-indicador-icones");
|
||
let dcIndicadorEmEdicao = null; // definição completa (id/chave/.../componentes) ou null = criando
|
||
let dcIndicadorIconeSelecionado = PID_DC_INDICADOR_ICONE_PADRAO;
|
||
let dcIndicadorComponenteSeq = 0;
|
||
|
||
// Grade de ícones é montada uma única vez (não depende da definição em
|
||
// edição, só a seleção ativa muda) — cada botão já é o próprio preview do
|
||
// ícone, sem precisar de nenhum outro elemento de preview separado.
|
||
PID_DC_INDICADOR_ICONES.forEach((icone) => {
|
||
const btn = document.createElement("button");
|
||
btn.type = "button";
|
||
btn.className = "dc-ind-icone-btn";
|
||
btn.setAttribute("data-dc-icone", icone.chave);
|
||
btn.setAttribute("aria-label", icone.rotulo);
|
||
btn.title = icone.rotulo;
|
||
btn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${icone.svg}</svg>`;
|
||
indicadorIconesEl.appendChild(btn);
|
||
});
|
||
|
||
function pidDcSelecionaIcone(chave) {
|
||
dcIndicadorIconeSelecionado = chave;
|
||
indicadorIconesEl.querySelectorAll(".dc-ind-icone-btn").forEach((btn) => {
|
||
btn.classList.toggle("is-selecionado", btn.getAttribute("data-dc-icone") === chave);
|
||
});
|
||
}
|
||
|
||
indicadorIconesEl.addEventListener("click", (event) => {
|
||
const btn = event.target.closest(".dc-ind-icone-btn");
|
||
if (btn) pidDcSelecionaIcone(btn.getAttribute("data-dc-icone"));
|
||
});
|
||
|
||
const PID_DC_COMP_TIPO_LABELS = {
|
||
contas: "Soma de contas do Balancete",
|
||
linha_dre: "Soma de linhas da DRE",
|
||
variacao_conta: "Variação de contas (atual − anterior)",
|
||
indicador: "Referência a outro indicador",
|
||
resultado_liquido: "Resultado Líquido da DRE (última linha)",
|
||
};
|
||
|
||
function pidDcRenderComponentePicker(linhaEl, tipo, valoresIniciais) {
|
||
const picker = linhaEl.querySelector(".dc-ind-comp-picker");
|
||
if (tipo === "contas" || tipo === "variacao_conta") {
|
||
const selecionados = valoresIniciais.contas_codigos || [];
|
||
const codigosNestaApuracao = new Set(apuracaoAtual.contas.map((conta) => conta.codigo));
|
||
// Um indicador pode ter sido criado a partir de outra apuração
|
||
// (empresa/competência diferente) — código selecionado que não
|
||
// existe aqui ainda precisa continuar marcado e ser salvo de volta
|
||
// do jeito que estava, senão editar um indicador desses nesta tela
|
||
// apagaria silenciosamente a conta que só existe na apuração
|
||
// original. Aparece como item extra, sinalizado, no topo da lista.
|
||
const orfaos = selecionados.filter((codigo) => !codigosNestaApuracao.has(codigo));
|
||
const selecionadosSet = new Set(selecionados);
|
||
picker.innerHTML = `
|
||
<input type="text" class="checklist-search" placeholder="Buscar conta por código ou descrição..." data-dc-comp-busca />
|
||
<div class="checklist-box" data-dc-comp-lista>
|
||
${orfaos.map((codigo) => `
|
||
<label class="checklist-item" data-dc-comp-item="${pidDcEscapeHtml(codigo.toLowerCase())}">
|
||
<input type="checkbox" value="${pidDcEscapeHtml(codigo)}" checked />
|
||
<span class="checklist-item__info"><span>${pidDcEscapeHtml(codigo)} — <em>conta não encontrada nesta apuração, mantida</em></span></span>
|
||
</label>
|
||
`).join("")}
|
||
${apuracaoAtual.contas.map((conta) => `
|
||
<label class="checklist-item" data-dc-comp-item="${pidDcEscapeHtml(`${conta.codigo} ${conta.descricao}`.toLowerCase())}">
|
||
<input type="checkbox" value="${pidDcEscapeHtml(conta.codigo)}" ${selecionadosSet.has(conta.codigo) ? "checked" : ""} />
|
||
<span class="checklist-item__info"><span>${pidDcEscapeHtml(conta.codigo)} — ${pidDcEscapeHtml(conta.descricao)}</span></span>
|
||
</label>
|
||
`).join("")}
|
||
</div>
|
||
`;
|
||
} else if (tipo === "linha_dre") {
|
||
const selecionados = valoresIniciais.linhas_dre_descricoes || [];
|
||
const descricoesNestaApuracao = new Set(apuracaoAtual.linhas_dre.map((linha) => linha.descricao));
|
||
const orfaos = selecionados.filter((descricao) => !descricoesNestaApuracao.has(descricao));
|
||
const selecionadosSet = new Set(selecionados);
|
||
picker.innerHTML = `
|
||
<input type="text" class="checklist-search" placeholder="Buscar linha da DRE..." data-dc-comp-busca />
|
||
<div class="checklist-box" data-dc-comp-lista>
|
||
${orfaos.map((descricao) => `
|
||
<label class="checklist-item" data-dc-comp-item="${pidDcEscapeHtml(descricao.toLowerCase())}">
|
||
<input type="checkbox" value="${pidDcEscapeHtml(descricao)}" checked />
|
||
<span class="checklist-item__info"><span>${pidDcEscapeHtml(descricao)} — <em>linha não encontrada nesta apuração, mantida</em></span></span>
|
||
</label>
|
||
`).join("")}
|
||
${apuracaoAtual.linhas_dre.map((linha) => `
|
||
<label class="checklist-item" data-dc-comp-item="${pidDcEscapeHtml(linha.descricao.toLowerCase())}">
|
||
<input type="checkbox" value="${pidDcEscapeHtml(linha.descricao)}" ${selecionadosSet.has(linha.descricao) ? "checked" : ""} />
|
||
<span class="checklist-item__info"><span>${pidDcEscapeHtml(linha.descricao)}</span></span>
|
||
</label>
|
||
`).join("")}
|
||
</div>
|
||
`;
|
||
} else if (tipo === "indicador") {
|
||
const atual = valoresIniciais.indicador_referenciado || "";
|
||
const chaveEditando = dcIndicadorEmEdicao ? dcIndicadorEmEdicao.chave : null;
|
||
const metadados = (dcIndicadoresAtual && dcIndicadoresAtual.metadados) || {};
|
||
const opcoes = Object.keys(metadados)
|
||
.filter((chave) => chave !== chaveEditando)
|
||
.sort((a, b) => metadados[a].nome.localeCompare(metadados[b].nome));
|
||
picker.innerHTML = `
|
||
<select class="dc-ind-comp-indicador">
|
||
<option value="">Selecione um indicador...</option>
|
||
${opcoes.map((chave) => `<option value="${pidDcEscapeHtml(chave)}" ${chave === atual ? "selected" : ""}>${pidDcEscapeHtml(metadados[chave].nome)}</option>`).join("")}
|
||
</select>
|
||
`;
|
||
} else if (tipo === "resultado_liquido") {
|
||
// Sem seleção nenhuma — sempre resolve pra última linha da DRE desta
|
||
// apuração, seja qual for o rótulo (evita quebrar quando o resultado
|
||
// muda de sinal e a linha passa de "LUCRO" pra "PREJUÍZO", ou
|
||
// vice-versa, ver dashboard_contabil/models.py).
|
||
picker.innerHTML = `<p class="dc-ind-formula-hint">Usa automaticamente o resultado líquido do período (última linha da DRE desta apuração), sem precisar escolher nada aqui.</p>`;
|
||
} else {
|
||
picker.innerHTML = "";
|
||
}
|
||
}
|
||
|
||
function pidDcCriaLinhaComponente(valoresIniciais) {
|
||
dcIndicadorComponenteSeq += 1;
|
||
const linha = document.createElement("div");
|
||
linha.className = "dc-ind-componente";
|
||
linha.dataset.dcCompId = String(dcIndicadorComponenteSeq);
|
||
const tipoInicial = valoresIniciais.tipo || "contas";
|
||
linha.innerHTML = `
|
||
<div class="dc-ind-componente__linha">
|
||
<div class="modal-field">
|
||
<label>Chave (usada na fórmula)</label>
|
||
<input type="text" class="dc-ind-comp-chave" placeholder="ex.: receita" value="${pidDcEscapeHtml(valoresIniciais.chave || "")}" />
|
||
</div>
|
||
<div class="modal-field">
|
||
<label>Tipo</label>
|
||
<select class="dc-ind-comp-tipo">
|
||
${Object.keys(PID_DC_COMP_TIPO_LABELS).map((tipo) => `<option value="${tipo}" ${tipo === tipoInicial ? "selected" : ""}>${PID_DC_COMP_TIPO_LABELS[tipo]}</option>`).join("")}
|
||
</select>
|
||
</div>
|
||
<button type="button" class="icon-btn dc-ind-comp-remover" aria-label="Remover componente" title="Remover componente">${PID_DC_ICON_LIXEIRA}</button>
|
||
</div>
|
||
<div class="dc-ind-comp-picker"></div>
|
||
`;
|
||
pidDcRenderComponentePicker(linha, tipoInicial, valoresIniciais);
|
||
return linha;
|
||
}
|
||
|
||
indicadorComponentesEl.addEventListener("change", (event) => {
|
||
if (event.target.classList.contains("dc-ind-comp-tipo")) {
|
||
const linha = event.target.closest(".dc-ind-componente");
|
||
pidDcRenderComponentePicker(linha, event.target.value, {});
|
||
}
|
||
});
|
||
indicadorComponentesEl.addEventListener("input", (event) => {
|
||
if (!event.target.hasAttribute("data-dc-comp-busca")) return;
|
||
const linha = event.target.closest(".dc-ind-componente");
|
||
const termo = event.target.value.trim().toLowerCase();
|
||
linha.querySelectorAll("[data-dc-comp-item]").forEach((item) => {
|
||
item.hidden = termo !== "" && !item.getAttribute("data-dc-comp-item").includes(termo);
|
||
});
|
||
});
|
||
indicadorComponentesEl.addEventListener("click", (event) => {
|
||
const removerBtn = event.target.closest(".dc-ind-comp-remover");
|
||
if (removerBtn) removerBtn.closest(".dc-ind-componente").remove();
|
||
});
|
||
|
||
document.getElementById("dc-indicador-add-componente-btn").addEventListener("click", () => {
|
||
indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente({}));
|
||
});
|
||
|
||
function pidDcAbrirIndicadorModal(definicao) {
|
||
dcIndicadorEmEdicao = definicao || null;
|
||
document.getElementById("dc-indicador-modal-titulo").textContent = definicao ? "Editar Indicador" : "Novo Indicador";
|
||
document.getElementById("dc-indicador-modal-nome").value = definicao ? definicao.nome : "";
|
||
document.getElementById("dc-indicador-modal-descricao").value = definicao ? definicao.descricao : "";
|
||
document.getElementById("dc-indicador-modal-formato").value = definicao ? definicao.formato : "indice";
|
||
document.getElementById("dc-indicador-modal-formula").value = definicao ? definicao.formula : "";
|
||
document.getElementById("dc-indicador-modal-formula-exibicao").value = definicao ? definicao.formula_exibicao : "";
|
||
document.getElementById("dc-indicador-modal-padrao").checked = definicao ? definicao.padrao : true;
|
||
pidDcSelecionaIcone(definicao ? definicao.icone : PID_DC_INDICADOR_ICONE_PADRAO);
|
||
document.getElementById("dc-indicador-modal-error").textContent = "";
|
||
document.getElementById("dc-indicador-modal-excluir-btn").hidden = !definicao;
|
||
document.getElementById("dc-indicador-preview").hidden = true;
|
||
|
||
indicadorComponentesEl.innerHTML = "";
|
||
const componentesIniciais = definicao ? definicao.componentes : [];
|
||
if (componentesIniciais.length) {
|
||
componentesIniciais.forEach((componente) => indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente(componente)));
|
||
} else {
|
||
indicadorComponentesEl.appendChild(pidDcCriaLinhaComponente({}));
|
||
}
|
||
indicadorModal.hidden = false;
|
||
}
|
||
|
||
function pidDcFecharIndicadorModal() {
|
||
indicadorModal.hidden = true;
|
||
dcIndicadorEmEdicao = null;
|
||
}
|
||
|
||
// Fechar por "Cancelar" ou clicando fora (overlay) sempre pede confirmação
|
||
// — pedido explícito do usuário, mesmo padrão já usado no modal de "Mais
|
||
// informações" (ajuda-aplicacao.js) e no antigo "Fechar sem salvar" do
|
||
// Cadastro de Regras de Plano de Saúde (ver "Modal de confirmação
|
||
// genérico" no CLAUDE.md da raiz). Só esses dois caminhos passam por
|
||
// aqui — depois de salvar/excluir com sucesso, o próprio fluxo chama
|
||
// `pidDcFecharIndicadorModal()` direto, sem perguntar (não há o que
|
||
// descartar).
|
||
async function pidDcFecharIndicadorModalComConfirmacao() {
|
||
if (!(await pidConfirm("Sair sem salvar as alterações?", { perigoso: true }))) return;
|
||
pidDcFecharIndicadorModal();
|
||
}
|
||
|
||
document.getElementById("dc-indicador-modal-cancel-btn").addEventListener("click", pidDcFecharIndicadorModalComConfirmacao);
|
||
indicadorModal.addEventListener("click", (event) => {
|
||
if (event.target === indicadorModal) pidDcFecharIndicadorModalComConfirmacao();
|
||
});
|
||
|
||
// Depois de criar/editar/excluir, além de recarregar os cards da aba
|
||
// "Dashboard", atualiza o hub também se ele estiver aberto por trás
|
||
// (o form de indicador é sempre stacked por cima dele, ver
|
||
// `.modal-overlay--top`/pidDcAbrirHubIndicadores) — sem isso, o hub
|
||
// ficaria com a lista desatualizada até ser reaberto.
|
||
async function pidDcAposSalvarOuExcluirIndicador() {
|
||
dcIndicadoresAtual = null;
|
||
await renderDashboardTab();
|
||
if (!indicadoresHubModal.hidden) await pidDcRenderHubIndicadores();
|
||
}
|
||
|
||
// Reaproveitada pelo "Salvar" e pelo "Calcular com esta apuração" — os
|
||
// dois precisam exatamente da mesma leitura do formulário (chave/tipo/
|
||
// seleção de cada linha de componente).
|
||
function pidDcColetaComponentesForm() {
|
||
return [...indicadorComponentesEl.querySelectorAll(".dc-ind-componente")].map((linha) => {
|
||
const tipo = linha.querySelector(".dc-ind-comp-tipo").value;
|
||
const chave = linha.querySelector(".dc-ind-comp-chave").value.trim();
|
||
const componente = { chave, tipo, contas_codigos: [], linhas_dre_descricoes: [], indicador_referenciado: "" };
|
||
if (tipo === "contas" || tipo === "variacao_conta") {
|
||
componente.contas_codigos = [...linha.querySelectorAll('[data-dc-comp-lista] input[type="checkbox"]:checked')].map(
|
||
(cb) => cb.value
|
||
);
|
||
} else if (tipo === "linha_dre") {
|
||
componente.linhas_dre_descricoes = [
|
||
...linha.querySelectorAll('[data-dc-comp-lista] input[type="checkbox"]:checked'),
|
||
].map((cb) => cb.value);
|
||
} else if (tipo === "indicador") {
|
||
const select = linha.querySelector(".dc-ind-comp-indicador");
|
||
componente.indicador_referenciado = select ? select.value : "";
|
||
}
|
||
return componente;
|
||
});
|
||
}
|
||
|
||
document.getElementById("dc-indicador-modal-save-btn").addEventListener("click", async () => {
|
||
const errorEl = document.getElementById("dc-indicador-modal-error");
|
||
errorEl.textContent = "";
|
||
|
||
const componentes = pidDcColetaComponentesForm();
|
||
|
||
const payload = {
|
||
nome: document.getElementById("dc-indicador-modal-nome").value.trim(),
|
||
descricao: document.getElementById("dc-indicador-modal-descricao").value.trim(),
|
||
formato: document.getElementById("dc-indicador-modal-formato").value,
|
||
formula: document.getElementById("dc-indicador-modal-formula").value.trim(),
|
||
formula_exibicao: document.getElementById("dc-indicador-modal-formula-exibicao").value.trim(),
|
||
padrao: document.getElementById("dc-indicador-modal-padrao").checked,
|
||
icone: dcIndicadorIconeSelecionado,
|
||
componentes,
|
||
};
|
||
|
||
if (!payload.nome) {
|
||
errorEl.textContent = "Informe o nome do indicador.";
|
||
return;
|
||
}
|
||
if (!componentes.length) {
|
||
errorEl.textContent = "Adicione pelo menos um componente.";
|
||
return;
|
||
}
|
||
if (componentes.some((c) => !c.chave)) {
|
||
errorEl.textContent = "Preencha a chave de todos os componentes.";
|
||
return;
|
||
}
|
||
if (!payload.formula) {
|
||
errorEl.textContent = "Informe a fórmula do indicador.";
|
||
return;
|
||
}
|
||
|
||
const saveBtn = document.getElementById("dc-indicador-modal-save-btn");
|
||
saveBtn.disabled = true;
|
||
try {
|
||
if (dcIndicadorEmEdicao) {
|
||
await pidAtualizarIndicadorContabil(dcIndicadorEmEdicao.id, payload);
|
||
} else {
|
||
await pidCriarIndicadorContabil(payload);
|
||
}
|
||
pidDcFecharIndicadorModal();
|
||
await pidDcAposSalvarOuExcluirIndicador();
|
||
} catch (e) {
|
||
errorEl.textContent = e.message;
|
||
} finally {
|
||
saveBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
// Calcula, contra a apuração aberta agora, o valor de cada componente e
|
||
// o resultado final — sem salvar nada — pro contador conferir se a
|
||
// fórmula pegou a conta/linha certa antes de confirmar. Reaproveita a
|
||
// mesma coleta do formulário do botão "Salvar" (`pidDcColetaComponentesForm`).
|
||
document.getElementById("dc-indicador-calcular-btn").addEventListener("click", async () => {
|
||
const previewEl = document.getElementById("dc-indicador-preview");
|
||
const componentes = pidDcColetaComponentesForm();
|
||
const formula = document.getElementById("dc-indicador-modal-formula").value.trim();
|
||
|
||
previewEl.hidden = false;
|
||
if (!componentes.length || componentes.some((c) => !c.chave) || !formula) {
|
||
previewEl.innerHTML = `<p class="dc-ind-preview__erro">Preencha a chave de todos os componentes e a fórmula antes de calcular.</p>`;
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
nome: document.getElementById("dc-indicador-modal-nome").value.trim() || "Pré-visualização",
|
||
descricao: "",
|
||
formato: document.getElementById("dc-indicador-modal-formato").value,
|
||
formula,
|
||
componentes,
|
||
};
|
||
|
||
const calcularBtn = document.getElementById("dc-indicador-calcular-btn");
|
||
calcularBtn.disabled = true;
|
||
previewEl.innerHTML = `<p class="dc-ind-preview__carregando">Calculando...</p>`;
|
||
try {
|
||
const resultado = await pidPreVisualizarIndicadorContabil(apuracaoAtual.id, payload);
|
||
if (resultado.erro) {
|
||
previewEl.innerHTML = `<p class="dc-ind-preview__erro">${pidDcEscapeHtml(resultado.erro)}</p>`;
|
||
} else {
|
||
previewEl.innerHTML = `
|
||
<ul class="dc-ind-preview__lista">
|
||
${resultado.componentes.map((c) => `
|
||
<li><span>${pidDcEscapeHtml(c.chave)}</span><span>${c.valor_formatado === null ? "—" : pidDcEscapeHtml(c.valor_formatado)}</span></li>
|
||
`).join("")}
|
||
</ul>
|
||
<div class="dc-ind-preview__resultado">Resultado: <strong>${resultado.resultado_formatado === null ? "—" : pidDcEscapeHtml(resultado.resultado_formatado)}</strong></div>
|
||
`;
|
||
}
|
||
} catch (e) {
|
||
previewEl.innerHTML = `<p class="dc-ind-preview__erro">${pidDcEscapeHtml(e.message)}</p>`;
|
||
} finally {
|
||
calcularBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
document.getElementById("dc-indicador-modal-excluir-btn").addEventListener("click", async () => {
|
||
if (!dcIndicadorEmEdicao) return;
|
||
if (
|
||
!(await pidConfirm(`Excluir o indicador "${dcIndicadorEmEdicao.nome}"? Essa ação não pode ser desfeita.`, {
|
||
perigoso: true,
|
||
}))
|
||
) {
|
||
return;
|
||
}
|
||
const errorEl = document.getElementById("dc-indicador-modal-error");
|
||
errorEl.textContent = "";
|
||
try {
|
||
await pidExcluirIndicadorContabil(dcIndicadorEmEdicao.id);
|
||
pidDcFecharIndicadorModal();
|
||
await pidDcAposSalvarOuExcluirIndicador();
|
||
} catch (e) {
|
||
errorEl.textContent = e.message;
|
||
}
|
||
});
|
||
|
||
// ---- Modal "Gerenciar Indicadores" (hub: lista todos os indicadores —
|
||
// padrão e não padrão — pra visualizar/editar/ativar, mesmo os que não
|
||
// estão aparecendo nesta apuração agora) ----
|
||
|
||
const indicadoresHubModal = document.getElementById("dc-indicadores-hub-modal");
|
||
|
||
function pidDcCriaLinhaHub(definicao, ativo, concluida) {
|
||
const linha = document.createElement("div");
|
||
linha.className = "dc-ind-hub-item";
|
||
linha.innerHTML = `
|
||
${
|
||
ativo === null
|
||
? `<span class="dc-ind-hub-item__spacer"></span>`
|
||
: `<input
|
||
type="checkbox"
|
||
class="dc-ind-hub-item__ativo"
|
||
data-dc-hub-toggle="${definicao.chave}"
|
||
${ativo ? "checked" : ""}
|
||
${concluida ? "disabled" : ""}
|
||
aria-label="Ativo nesta apuração"
|
||
title="Ativo nesta apuração"
|
||
/>`
|
||
}
|
||
<button type="button" class="dc-ind-hub-item__nome" data-dc-hub-ver="${definicao.id}">${pidDcEscapeHtml(definicao.nome)}</button>
|
||
<button
|
||
type="button"
|
||
class="icon-btn"
|
||
data-dc-hub-editar="${definicao.id}"
|
||
aria-label="Editar"
|
||
title="Editar"
|
||
${concluida ? "disabled" : ""}
|
||
>${PID_DC_ICON_EDITAR}</button>
|
||
`;
|
||
return linha;
|
||
}
|
||
|
||
async function pidDcRenderHubIndicadores() {
|
||
const listaPadrao = document.getElementById("dc-ind-hub-lista-padrao");
|
||
const listaNaoPadrao = document.getElementById("dc-ind-hub-lista-nao-padrao");
|
||
const vazioPadrao = document.getElementById("dc-ind-hub-vazio-padrao");
|
||
const vazioNaoPadrao = document.getElementById("dc-ind-hub-vazio-nao-padrao");
|
||
listaPadrao.innerHTML = "";
|
||
listaNaoPadrao.innerHTML = "";
|
||
|
||
let definicoes;
|
||
try {
|
||
definicoes = await pidBuscarDefinicoesIndicadoresContabil();
|
||
} catch (e) {
|
||
await pidAlert(e.message);
|
||
return;
|
||
}
|
||
|
||
const selecionados = new Set((dcIndicadoresAtual && dcIndicadoresAtual.indicadores_selecionados) || []);
|
||
const concluida = apuracaoAtual.status === "concluida";
|
||
const padrao = definicoes.filter((d) => d.padrao);
|
||
const naoPadrao = definicoes.filter((d) => !d.padrao);
|
||
|
||
vazioPadrao.hidden = padrao.length > 0;
|
||
vazioNaoPadrao.hidden = naoPadrao.length > 0;
|
||
padrao.forEach((definicao) => listaPadrao.appendChild(pidDcCriaLinhaHub(definicao, null, concluida)));
|
||
naoPadrao.forEach((definicao) =>
|
||
listaNaoPadrao.appendChild(pidDcCriaLinhaHub(definicao, selecionados.has(definicao.chave), concluida))
|
||
);
|
||
}
|
||
|
||
async function pidDcAbrirHubIndicadores() {
|
||
indicadoresHubModal.hidden = false;
|
||
await pidDcRenderHubIndicadores();
|
||
}
|
||
|
||
document.getElementById("dc-indicadores-gerenciar-btn").addEventListener("click", pidDcAbrirHubIndicadores);
|
||
document.getElementById("dc-indicadores-hub-fechar-btn").addEventListener("click", () => {
|
||
indicadoresHubModal.hidden = true;
|
||
});
|
||
indicadoresHubModal.addEventListener("click", (event) => {
|
||
if (event.target === indicadoresHubModal) indicadoresHubModal.hidden = true;
|
||
});
|
||
document.getElementById("dc-indicadores-hub-novo-btn").addEventListener("click", () => pidDcAbrirIndicadorModal(null));
|
||
|
||
indicadoresHubModal.addEventListener("click", async (event) => {
|
||
const verBtn = event.target.closest("[data-dc-hub-ver]");
|
||
if (verBtn) {
|
||
const definicaoId = Number(verBtn.getAttribute("data-dc-hub-ver"));
|
||
try {
|
||
const definicao = await pidBuscarIndicadorContabilDefinicao(definicaoId);
|
||
pidDcAbrirFormulaModal({
|
||
nome: definicao.nome,
|
||
descricao: definicao.descricao,
|
||
formula_texto: definicao.formula,
|
||
});
|
||
} catch (e) {
|
||
await pidAlert(e.message);
|
||
}
|
||
return;
|
||
}
|
||
const editarBtn = event.target.closest("[data-dc-hub-editar]");
|
||
if (editarBtn) {
|
||
if (editarBtn.disabled) return;
|
||
const definicaoId = Number(editarBtn.getAttribute("data-dc-hub-editar"));
|
||
try {
|
||
const definicao = await pidBuscarIndicadorContabilDefinicao(definicaoId);
|
||
pidDcAbrirIndicadorModal(definicao);
|
||
} catch (e) {
|
||
await pidAlert(e.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
indicadoresHubModal.addEventListener("change", async (event) => {
|
||
const checkbox = event.target.closest("[data-dc-hub-toggle]");
|
||
if (!checkbox || checkbox.disabled) return;
|
||
const chave = checkbox.getAttribute("data-dc-hub-toggle");
|
||
const atual = new Set(dcIndicadoresAtual.indicadores_selecionados || []);
|
||
if (checkbox.checked) atual.add(chave);
|
||
else atual.delete(chave);
|
||
checkbox.disabled = true;
|
||
try {
|
||
await pidAtualizarIndicadoresSelecionadosContabil(apuracaoAtual.id, [...atual]);
|
||
dcIndicadoresAtual = null;
|
||
await renderDashboardTab();
|
||
await pidDcRenderHubIndicadores();
|
||
} catch (e) {
|
||
checkbox.checked = !checkbox.checked;
|
||
checkbox.disabled = false;
|
||
await pidAlert(e.message);
|
||
}
|
||
});
|
||
|
||
// Lista consolidada da aba "Dashboard": as observações vigentes das três
|
||
// tabelas (Balancete/DRE/Análise Vertical) mais as justificativas de
|
||
// auditoria — pré-visualização do que vai (ou não) pro relatório do
|
||
// cliente. Observação segue o histórico (`ContabilObservacao`), auditoria
|
||
// continua presa à apuração (`ContabilAchado.oculto_no_relatorio`), então
|
||
// os dois tipos de item têm ações diferentes.
|
||
function renderDashboardObservacoes() {
|
||
const lista = document.getElementById("dc-dash-observacoes-list");
|
||
const empty = document.getElementById("dc-dash-observacoes-empty");
|
||
const concluida = apuracaoAtual.status === "concluida";
|
||
|
||
const observacoes = dcObsAplicaFiltro(dcObservacoes);
|
||
const achados = apuracaoAtual.achados.filter((achado) => {
|
||
if (!achado.observacao_contador) return false;
|
||
if (dcObsFiltro === "cliente") return !achado.oculto_no_relatorio;
|
||
if (dcObsFiltro === "internas") return achado.oculto_no_relatorio;
|
||
return true;
|
||
});
|
||
|
||
const html = [
|
||
...observacoes.map((obs) => dcObsResumoItemHtml(obs, PID_DC_OBS_ORIGENS[obs.alvo_tipo], concluida)),
|
||
...achados.map(
|
||
(achado) => `
|
||
<div class="dc-dash-obs${achado.oculto_no_relatorio ? " is-oculto" : ""}">
|
||
<div class="dc-dash-obs__info">
|
||
<span class="dc-dash-obs__origem">Auditoria</span>
|
||
<span class="dc-dash-obs__referencia">${pidDcEscapeHtml(achado.titulo)}</span>
|
||
<p class="dc-dash-obs__texto">${pidDcEscapeHtml(achado.observacao_contador)}</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="icon-btn dc-dash-toggle-btn"
|
||
data-dc-dash-achado-toggle="${achado.id}"
|
||
aria-label="${achado.oculto_no_relatorio ? "Mostrar" : "Ocultar"} no relatório"
|
||
title="${achado.oculto_no_relatorio ? "Mostrar no relatório" : "Ocultar do relatório"}"
|
||
${concluida ? "disabled" : ""}
|
||
>
|
||
${achado.oculto_no_relatorio ? PID_DC_ICON_OLHO_FECHADO : PID_DC_ICON_OLHO_ABERTO}
|
||
</button>
|
||
</div>
|
||
`
|
||
),
|
||
];
|
||
|
||
empty.hidden = html.length > 0;
|
||
lista.innerHTML = html.join("");
|
||
}
|
||
|
||
document.getElementById("dc-dash-observacoes-list").addEventListener("click", async (event) => {
|
||
const achadoBtn = event.target.closest("[data-dc-dash-achado-toggle]");
|
||
if (achadoBtn) {
|
||
if (achadoBtn.disabled) return;
|
||
const id = Number(achadoBtn.getAttribute("data-dc-dash-achado-toggle"));
|
||
const achado = apuracaoAtual.achados.find((a) => a.id === id);
|
||
achadoBtn.disabled = true;
|
||
try {
|
||
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) {
|
||
achadoBtn.disabled = false;
|
||
await pidAlert(e.message);
|
||
}
|
||
return;
|
||
}
|
||
await dcTrataCliqueObservacao(event, "conta");
|
||
});
|
||
|
||
// ---- Resumo de observações no final de cada aba ----
|
||
// Cada aba (Balancete/DRE/Análise Vertical) lista as observações vigentes
|
||
// daquela seção — as escritas nesta competência e as herdadas do histórico
|
||
// —, com a assinatura de quem escreveu, o botão de olho (mostrar ao
|
||
// cliente) e o de encerrar/reativar, sem precisar abrir conta por conta.
|
||
// Os chips "Todas / Visíveis ao cliente / Internas" filtram as quatro
|
||
// listas ao mesmo tempo (ver dcObsFiltro).
|
||
|
||
function dcObsResumoItemHtml(obs, origem, concluida) {
|
||
const selos = [];
|
||
if (obs.historica) selos.push('<span class="dc-obs-selo dc-obs-selo--historica">Histórico</span>');
|
||
if (obs.encerrada) {
|
||
selos.push(
|
||
`<span class="dc-obs-selo dc-obs-selo--encerrada">Encerrada em ${pidDcFormatCompetencia(obs.encerrada_em_competencia)}</span>`
|
||
);
|
||
}
|
||
const acaoEncerrar = concluida
|
||
? ""
|
||
: obs.encerrada
|
||
? `<button type="button" class="icon-btn" data-dc-obs-reativar="${obs.id}" title="Voltar a exibir nas próximas competências" aria-label="Reativar observação">${PID_DC_ICON_REATIVAR}</button>`
|
||
: `<button type="button" class="icon-btn" data-dc-obs-encerrar="${obs.id}" title="Ocultar das próximas competências (continua visível nesta)" aria-label="Encerrar observação">${PID_DC_ICON_ENCERRAR}</button>`;
|
||
return `
|
||
<div class="dc-dash-obs${obs.mostrar_ao_cliente ? "" : " is-oculto"}">
|
||
<div class="dc-dash-obs__info">
|
||
${origem ? `<span class="dc-dash-obs__origem">${pidDcEscapeHtml(origem)}</span>` : ""}
|
||
<span class="dc-dash-obs__referencia">${pidDcEscapeHtml(obs.alvo_rotulo || obs.alvo_chave)}</span>
|
||
<p class="dc-dash-obs__texto">${pidDcEscapeHtml(obs.texto)}</p>
|
||
<span class="dc-obs-item__assinatura">${pidDcEscapeHtml(obs.criado_por_nome || "Usuário removido")} · ${pidDcFormatData(obs.criado_em)} · competência ${pidDcFormatCompetencia(obs.competencia_origem)}</span>
|
||
${selos.length ? `<span class="dc-obs-item__selos">${selos.join("")}</span>` : ""}
|
||
</div>
|
||
<div class="dc-obs-item__acoes">
|
||
<button
|
||
type="button"
|
||
class="icon-btn dc-dash-toggle-btn"
|
||
data-dc-obs-visibilidade="${obs.id}"
|
||
aria-label="${obs.mostrar_ao_cliente ? "Ocultar" : "Mostrar"} no relatório"
|
||
title="${obs.mostrar_ao_cliente ? "Ocultar do relatório do cliente" : "Mostrar no relatório do cliente"}"
|
||
${concluida ? "disabled" : ""}
|
||
>
|
||
${obs.mostrar_ao_cliente ? PID_DC_ICON_OLHO_ABERTO : PID_DC_ICON_OLHO_FECHADO}
|
||
</button>
|
||
${acaoEncerrar}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function dcRenderObsResumo(tipo, idLista, idVazio, idContador) {
|
||
const lista = document.getElementById(idLista);
|
||
const empty = document.getElementById(idVazio);
|
||
const count = document.getElementById(idContador);
|
||
const concluida = apuracaoAtual.status === "concluida";
|
||
const itens = dcObsAplicaFiltro(dcObservacoesDoTipo(tipo));
|
||
count.textContent = String(itens.length);
|
||
empty.hidden = itens.length > 0;
|
||
lista.innerHTML = itens.map((obs) => dcObsResumoItemHtml(obs, null, concluida)).join("");
|
||
}
|
||
|
||
function renderContasObsResumo() {
|
||
dcRenderObsResumo("conta", "dc-contas-obs-list", "dc-contas-obs-empty", "dc-contas-obs-count");
|
||
}
|
||
|
||
function renderDreObsResumo() {
|
||
dcRenderObsResumo("dre", "dc-dre-obs-list", "dc-dre-obs-empty", "dc-dre-obs-count");
|
||
}
|
||
|
||
function renderAnaliseVerticalObsResumo() {
|
||
dcRenderObsResumo("analise_vertical", "dc-av-obs-list", "dc-av-obs-empty", "dc-av-obs-count");
|
||
}
|
||
|
||
["dc-contas-obs-list", "dc-dre-obs-list", "dc-av-obs-list"].forEach((id) => {
|
||
document.getElementById(id).addEventListener("click", async (event) => {
|
||
await dcTrataCliqueObservacao(event, "conta");
|
||
});
|
||
});
|
||
|
||
// ---- 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");
|
||
});
|