diff --git a/.claude/settings.json b/.claude/settings.json
index 83533e5..efd9ca5 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -80,7 +80,9 @@
"Bash(\"./.venv/Scripts/python.exe\" \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\75800899-5860-4fc9-a89a-41027f847563\\\\scratchpad\\\\check_amil_pipeline_full.py\")",
"Bash(\"./.venv/Scripts/python.exe\" \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\75800899-5860-4fc9-a89a-41027f847563\\\\scratchpad\\\\check_dup_cpf.py\")",
"Bash(\"./.venv/Scripts/python.exe\" manage.py makemigrations --check --dry-run)",
- "Bash(\"./.venv/Scripts/pip.exe\" install *)"
+ "Bash(\"./.venv/Scripts/pip.exe\" install *)",
+ "PowerShell(node --check \"static/js/dashboard-contabil.js\")",
+ "Bash(.venv/Scripts/python.exe -m py_compile portal_api/dashboard_contabil/regras.py)"
]
}
}
diff --git a/portal_api/dashboard_contabil/CHANGELOG.md b/portal_api/dashboard_contabil/CHANGELOG.md
index 3a42e6a..e2420d9 100644
--- a/portal_api/dashboard_contabil/CHANGELOG.md
+++ b/portal_api/dashboard_contabil/CHANGELOG.md
@@ -300,3 +300,15 @@ Segundo pedido, mesma rodada: quando uma conta/linha "filha" tem observação e
### 136. Coluna "Conta" da revisão mostrava a Classificação, não o número da conta
Usuário comparou com o PDF original (que tem as duas colunas, "Conta" e "S Classificação") e reportou que a aba Balancete da tela de revisão só mostrava a Classificação (`codigo`) numa coluna rotulada "Conta" — o número interno da conta no Questor (`conta_numero`, já extraído e salvo desde sempre, só nunca exibido) não aparecia em lugar nenhum. Confirmado com o usuário que o pedido era só pra tela de revisão (não pro relatório "Gerar Dashboard" que vai pro cliente). `dashboard-contabil.html` ganhou uma coluna nova ("Conta", `conta_numero`) antes da já existente, renomeada pra "Classificação" (`codigo`); `renderContas()` (`dashboard-contabil.js`) passou a emitir as duas células. Ajustes de acompanhamento: colspan do editor inline de observação (7 → 8) e os seletores CSS que dependiam de posição de coluna (`.dc-contas-table td:nth-child(...)`, alinhamento numérico das colunas de valor e o estilo apagado/`nowrap` da(s) coluna(s) de identificação da conta) deslocados em uma posição.
+
+### 137. Histórico de execuções ganhou ordenação e filtro por coluna, igual Importação de Plano de Saúde
+
+Usuário pediu que a tabela de execuções (histórico de análises, `#dc-list-table`) tivesse a mesma ordenação/filtro por coluna já existente na tabela equivalente de Importação de Plano de Saúde, com a última execução aparecendo primeiro por padrão e um botão de limpar filtros. Mecanismo portado 1:1 (mesmas classes/lógica, prefixo `dc-` em vez de `ips-`): funil "estilo Excel" no cabeçalho de Empresa/Status/Criado por (busca + checklist de valores distintos), as 6 colunas ordenáveis por clique no `
`, ordenação padrão por `criado_em` decrescente (antes disso a ordem vinha só do `Meta.ordering` do model, que prioriza competência antes de data de criação) e botão "borracha" que limpa os filtros e volta a ordenação pro padrão de uma vez. `carregarLista()` passou a só buscar a API; a filtragem/ordenação/render virou uma função separada (`renderList()`) que roda sobre o array já carregado, sem round-trip novo a cada clique. Sem paginação, diferente da outra tela (não foi pedida, e o histórico tende a ser mais curto).
+
+**Ajuste na mesma rodada, antes do usuário terminar de testar**: "Competência" também ganhou o funil de filtro (nasceu só ordenável) — indexado pela data ISO bruta, com o popup mostrando o rótulo `MM/AAAA` já usado no resto da tela. De brinde, a ordenação dos valores dentro de qualquer popup de filtro passou a usar o valor bruto em vez do rótulo formatado, porque ordenar "Competência" pelo rótulo (`localeCompare` de "MM/AAAA") agruparia por mês antes do ano; a data ISO já ordena cronologicamente certo por comparação de string. Detalhe técnico completo no `CLAUDE.md` desta pasta.
+
+### 138. Bug real: valores em R$ dentro do texto dos achados sem formatação brasileira
+
+Usuário reportou, com print da aba Observações da revisão, que valores monetários dentro do texto dos achados apareciam sem padronização (ex.: "R$ 643545.85" em vez de "R$ 643.545,85"), inconsistente com o padrão já usado em Balancete/DRE/relatório "Gerar Dashboard". Causa raiz: `regras.py` interpola `Decimal` direto num f-string (`f"R$ {conta.saldo_atual}"`) pra montar `AchadoDetectado.mensagem` — usa a formatação padrão do Python (sem separador de milhar, ponto decimal), nunca o padrão BR. Corrigido com um helper novo, `_moeda()` (`regras.py`), duplicado localmente em vez de importado — mesmo padrão já usado (por arquivo) em `indicadores/recibo.py`/`custo_contratacao/pdf.py`/`templatetags/contabil_extras.py`, já que este pacote é Python puro. As 9 mensagens de achado que interpolavam `R$ {valor}` direto foram todas ajustadas.
+
+Só vale pra achados gerados dali em diante — um achado já persistido mantém o texto antigo até a apuração ser reprocessada ou recriada; sem migração de dados pra reformatar texto já gravado (parsear número dentro de frase livre por regex arriscaria confundir valor monetário com código de conta, ex. "1.01.01.001"). Fora do escopo desta rodada: `regra_variacao_atipica_dre` ainda formata percentual com ponto decimal (`"12.34%"`, não `"12,34%"`) — mesma família de bug, não pedida desta vez. Detalhe técnico completo no `CLAUDE.md` desta pasta.
diff --git a/portal_api/dashboard_contabil/CLAUDE.md b/portal_api/dashboard_contabil/CLAUDE.md
index d6d8d53..e86d27d 100644
--- a/portal_api/dashboard_contabil/CLAUDE.md
+++ b/portal_api/dashboard_contabil/CLAUDE.md
@@ -436,4 +436,25 @@ Validado de duas formas: (1) ponta a ponta com `Client.force_login()` em transa
**Migração de dados**: a `0071` cria o model, copia cada observação preenchida das três tabelas (autor e data vêm da apuração, a melhor aproximação disponível — o modelo antigo não guardava nada disso por observação; `oculta_no_relatorio` vira `mostrar_ao_cliente` invertido) e só então remove os seis campos antigos. Em produção eram 3 observações (2 de conta, 1 de DRE), todas migradas e conferidas depois de aplicar.
+### Ordenação e filtro por coluna no histórico (`#dc-list-table`)
+
+Pedido explícito do usuário pra ter a mesma experiência de `#ips-list-table` (Importação de Plano de Saúde) — mesmo mecanismo, client-side, portado 1:1 e renomeado com o prefixo `dc-`/`PID_DC_*` (não compartilhado entre os dois arquivos JS/CSS, cada tela carrega só o próprio):
+
+- `carregarLista()` agora só busca a API e guarda em `dcListaApuracoes` (module-scope); `renderList()` (nova, sem fetch) filtra (`dcApuracaoPassaNosFiltros`) + ordena (`dcComparaValoresLista`, numérico quando os dois valores parecem número, senão `localeCompare` pt-BR) + desenha as `
` — chamada tanto por `carregarLista()` quanto por qualquer mudança de ordenação/filtro, sem precisar de um novo round-trip à API.
+- **Ordenação padrão**: `criado_em` decrescente (`dcListSortDir = -1`) — a última execução aparece primeiro, decisão explícita do usuário (antes disso a ordem vinha só do backend, `ContabilApuracao.Meta.ordering = ["-competencia", "-criado_em"]`, que prioriza competência antes de data de criação).
+- **Colunas ordenáveis** (as 6 do histórico, `data-sort` no `
`): `empresa` (por `codigo_empresa`), `competencia`, `observacoes` (`total_achados_pendentes`), `status`, `criado_por` (`criado_por_nome`), `criado_em`.
+- **Colunas com filtro "estilo Excel"** (funil no `
`, popup com busca + checklist, reaproveita `.checklist-*` de `components.css`): `empresa`, `competencia`, `status`, `criado_por` — deixa de fora só `observacoes` (contagem, não uma dimensão de agrupamento) e `criado_em` (data/hora de criação, granularidade fina demais pra um checklist de valores distintos fazer sentido); `competencia` entrou a pedido explícito do usuário numa rodada seguinte (nasceu só ordenável, no mesmo recorte de Importação de Plano de Saúde, que deixa `criado_em` de fora do filtro — mas lá não existe uma coluna de "período" recorrente como esta, em que várias apurações de empresas diferentes tendem a cair na mesma competência). O filtro de "empresa" indexa por `codigo_empresa` (não pelo texto combinado "código - nome" da célula), mas o popup mostra o rótulo completo (`dcLabelValorFiltroLista` busca o `nome_empresa` correspondente em `dcListaApuracoes`) pra continuar identificável; o de "competencia" indexa pela data ISO bruta e mostra o rótulo `MM/AAAA` (`pidDcFormatCompetencia`). **`valoresDistintos()` ordena pelo valor bruto** (`dcComparaValoresLista`), não pelo rótulo formatado — importa justamente pra "competencia", já que ordenar pelo rótulo "MM/AAAA" agruparia por mês antes do ano (ex.: "01/2026" antes de "12/2025"), enquanto a string ISO "AAAA-MM-DD" já ordena cronologicamente certo por comparação simples.
+- Botão "borracha" (`#dc-list-reset-btn`, dentro do último `
` vazio, ao lado da coluna de ações) limpa os quatro filtros e volta a ordenação pro padrão (`criado_em` desc) de uma vez.
+- Sem paginação (diferente de Importação de Plano de Saúde) — não foi pedida, e o histórico desta ferramenta tende a ser mais curto (uma linha por empresa+competência).
+
+### Valores monetários dos achados sem formatação BR (bug real, rodada seguinte)
+
+Usuário reportou (com print da aba Observações da revisão) que os valores em R$ dentro do texto dos achados apareciam sem separador de milhar e com ponto decimal (ex.: "R$ 643545.85"), inconsistente com o padrão brasileiro (ponto de milhar, vírgula decimal) já usado em Balancete/DRE (`pidDcFormatMoeda()` no frontend) e no relatório "Gerar Dashboard" (`moeda()` em `contabil_extras.py`). Causa raiz: `regras.py` interpola `Decimal` **direto** num f-string (`f"R$ {conta.saldo_atual}"`) pra montar `AchadoDetectado.mensagem` — isso usa a formatação padrão do Python (`str(Decimal(...))`), que nunca tem separador de milhar.
+
+Corrigido com um helper novo, `_moeda(valor: Decimal) -> str` (`regras.py`), duplicado localmente em vez de importado — mesmo padrão já usado (por arquivo) em `indicadores/recibo.py`/`custo_contratacao/pdf.py`/`templatetags/contabil_extras.py`, porque este pacote é Python puro (sem tocar no ORM/app registry do Django, ver docstring do módulo). Todas as 9 mensagens de achado que interpolavam `R$ {valor}` diretamente (`regra_balanceamento_ativo_passivo`, `regra_debito_credito_divergente`, `regra_saldo_negativo_caixa`, `regra_conta_transitoria_com_saldo`, `regra_conta_deveria_zerar`, `regra_saldo_sinal_invertido` — as duas variantes —, `regra_lucro_balancete_diverge_dre`, `regra_descricao_generica`) passaram a usar `_moeda()`.
+
+**Não corrigido nesta rodada** (fora do escopo do pedido, mesma família de bug): `regra_variacao_atipica_dre` formata percentual com `f"{valor:.2f}%"` (ponto decimal, ex. "12.34%"), também inconsistente com o padrão BR (`12,34%`) — se o usuário pedir, é o mesmo tipo de ajuste.
+
+**Só vale pra achados gerados dali em diante** — um achado já persistido em produção mantém o texto antigo (sem formatação) até a apuração ser reprocessada (`reprocessar()`/`_contabil_sincroniza_achados()` em `views.py` sobrescreve `mensagem` de um achado que ainda dispara na mesma conta+regra) ou até uma nova apuração da mesma empresa ser criada do zero; não foi escrita nenhuma migração de dados pra reformatar o texto já gravado (parsear números dentro de frase livre por regex é arriscado — o mesmo texto tem números que não são valores, como código de conta "1.01.01.001").
+
**Testado ponta a ponta via `Client.force_login()`** dentro de uma transação com rollback forçado: listagem por vigência, criação com chave derivada no servidor, alvo de outra apuração recusado, edição de texto, bloqueio do texto quando a apuração de origem está concluída (com a visibilidade ainda alternável nesse mesmo caso), encerrar e reativar com as fronteiras de competência conferidas nos dois sentidos, herança numa competência seguinte (observação marcada como histórica) e o relatório gerado nos dois meses, incluindo a conferência de que observação interna não vaza pro relatório do cliente. Nada gravado em produção além da própria migração.
diff --git a/portal_api/dashboard_contabil/regras.py b/portal_api/dashboard_contabil/regras.py
index fc67d17..5109a73 100644
--- a/portal_api/dashboard_contabil/regras.py
+++ b/portal_api/dashboard_contabil/regras.py
@@ -56,6 +56,19 @@ VARIACAO_VALOR_MINIMO = Decimal("1000") # ignora variações abaixo disso, mesm
VARIACAO_AV_PONTOS_PERCENTUAIS_MINIMO = Decimal("1")
+def _moeda(valor: Decimal) -> str:
+ """Formata no padrão brasileiro (ponto de milhar, vírgula decimal) —
+ mesmo espírito do helper `_moeda()`/`moeda()` já duplicado por arquivo em
+ `indicadores/recibo.py`, `custo_contratacao/pdf.py` e
+ `templatetags/contabil_extras.py`; duplicado aqui em vez de importado
+ porque este pacote é Python puro, sem depender do app registry do Django
+ (ver docstring do módulo). Interpolar `Decimal` direto num f-string (ex.:
+ `f"R$ {valor}"`) usa a formatação padrão do Python — ponto decimal, sem
+ separador de milhar — que é exatamente o bug que este helper evita."""
+ texto_us = f"{valor:,.2f}"
+ return f"R$ {texto_us.replace(',', '_').replace('.', ',').replace('_', '.')}"
+
+
def regra_balanceamento_ativo_passivo(atual: ResultadoExtracao, historico: list[SnapshotHistorico]) -> list[AchadoDetectado]:
ativo = next((c for c in atual.contas if c.codigo == "1"), None)
passivo = next((c for c in atual.contas if c.codigo == "2"), None)
@@ -70,8 +83,8 @@ def regra_balanceamento_ativo_passivo(atual: ResultadoExtracao, historico: list[
severidade=SEVERIDADE_ALTA,
titulo="Ativo não bate com Passivo",
mensagem=(
- f"Saldo do Ativo (R$ {ativo.saldo_atual}) não coincide com o do Passivo "
- f"(R$ {-passivo.saldo_atual}) — diferença de R$ {abs(diferenca)}."
+ f"Saldo do Ativo ({_moeda(ativo.saldo_atual)}) não coincide com o do Passivo "
+ f"({_moeda(-passivo.saldo_atual)}) — diferença de {_moeda(abs(diferenca))}."
),
valor_referencia=diferenca,
)
@@ -91,8 +104,8 @@ def regra_debito_credito_divergente(atual: ResultadoExtracao, historico: list[Sn
severidade=SEVERIDADE_ALTA,
titulo="Débito total diferente do crédito total",
mensagem=(
- f"Soma de débitos do período (R$ {soma_debito}) não bate com a soma de "
- f"créditos (R$ {soma_credito}) — diferença de R$ {abs(diferenca)}."
+ f"Soma de débitos do período ({_moeda(soma_debito)}) não bate com a soma de "
+ f"créditos ({_moeda(soma_credito)}) — diferença de {_moeda(abs(diferenca))}."
),
valor_referencia=diferenca,
)
@@ -108,7 +121,7 @@ def regra_saldo_negativo_caixa(atual: ResultadoExtracao, historico: list[Snapsho
regra="saldo_negativo_caixa",
severidade=SEVERIDADE_ALTA,
titulo="Saldo de caixa negativo",
- mensagem=f'Conta "{conta.descricao}" ({conta.codigo}) com saldo negativo de R$ {conta.saldo_atual}.',
+ mensagem=f'Conta "{conta.descricao}" ({conta.codigo}) com saldo negativo de {_moeda(conta.saldo_atual)}.',
codigo_conta=conta.codigo,
valor_referencia=conta.saldo_atual,
)
@@ -127,7 +140,7 @@ def regra_conta_transitoria_com_saldo(atual: ResultadoExtracao, historico: list[
regra="conta_transitoria_com_saldo",
severidade=SEVERIDADE_MEDIA,
titulo="Conta transitória com saldo",
- mensagem=f'Conta transitória "{conta.descricao}" ({conta.codigo}) deveria estar zerada e está com R$ {conta.saldo_atual}.',
+ mensagem=f'Conta transitória "{conta.descricao}" ({conta.codigo}) deveria estar zerada e está com {_moeda(conta.saldo_atual)}.',
codigo_conta=conta.codigo,
valor_referencia=conta.saldo_atual,
)
@@ -147,7 +160,7 @@ def regra_conta_deveria_zerar(atual: ResultadoExtracao, historico: list[Snapshot
regra="conta_deveria_zerar",
severidade=SEVERIDADE_MEDIA,
titulo="Conta que deveria estar zerada",
- mensagem=f'Conta "{conta.descricao}" ({conta.codigo}) normalmente fica zerada todo mês e está com saldo de R$ {conta.saldo_atual}.',
+ mensagem=f'Conta "{conta.descricao}" ({conta.codigo}) normalmente fica zerada todo mês e está com saldo de {_moeda(conta.saldo_atual)}.',
codigo_conta=conta.codigo,
valor_referencia=conta.saldo_atual,
)
@@ -170,7 +183,7 @@ def regra_saldo_sinal_invertido(atual: ResultadoExtracao, historico: list[Snapsh
regra="saldo_sinal_invertido",
severidade=SEVERIDADE_MEDIA,
titulo="Conta do Ativo com saldo credor",
- mensagem=f'Conta do Ativo "{conta.descricao}" ({conta.codigo}) está com saldo credor de R$ {-conta.saldo_atual}.',
+ mensagem=f'Conta do Ativo "{conta.descricao}" ({conta.codigo}) está com saldo credor de {_moeda(-conta.saldo_atual)}.',
codigo_conta=conta.codigo,
valor_referencia=conta.saldo_atual,
)
@@ -181,7 +194,7 @@ def regra_saldo_sinal_invertido(atual: ResultadoExtracao, historico: list[Snapsh
regra="saldo_sinal_invertido",
severidade=SEVERIDADE_MEDIA,
titulo="Conta do Passivo com saldo devedor",
- mensagem=f'Conta do Passivo "{conta.descricao}" ({conta.codigo}) está com saldo devedor de R$ {conta.saldo_atual}.',
+ mensagem=f'Conta do Passivo "{conta.descricao}" ({conta.codigo}) está com saldo devedor de {_moeda(conta.saldo_atual)}.',
codigo_conta=conta.codigo,
valor_referencia=conta.saldo_atual,
)
@@ -210,7 +223,7 @@ def regra_lucro_balancete_diverge_dre(atual: ResultadoExtracao, historico: list[
titulo="Lucro do balancete diferente do lucro da DRE",
mensagem=(
f'Resultado do exercício no Balancete ("{conta_lucro.descricao}", {conta_lucro.codigo}) é de '
- f"R$ {lucro_balancete}, mas a DRE aponta R$ {lucro_dre} — diferença de R$ {abs(diferenca)}."
+ f"{_moeda(lucro_balancete)}, mas a DRE aponta {_moeda(lucro_dre)} — diferença de {_moeda(abs(diferenca))}."
),
codigo_conta=conta_lucro.codigo,
valor_referencia=diferenca,
@@ -227,7 +240,7 @@ def regra_descricao_generica(atual: ResultadoExtracao, historico: list[SnapshotH
regra="descricao_generica",
severidade=SEVERIDADE_BAIXA,
titulo='Conta com descrição genérica ("DIVERSOS")',
- mensagem=f'Conta {conta.codigo} está descrita apenas como "DIVERSOS", com saldo de R$ {conta.saldo_atual} — recomenda-se detalhar em conta analítica própria.',
+ mensagem=f'Conta {conta.codigo} está descrita apenas como "DIVERSOS", com saldo de {_moeda(conta.saldo_atual)} — recomenda-se detalhar em conta analítica própria.',
codigo_conta=conta.codigo,
valor_referencia=conta.saldo_atual,
)
diff --git a/static/css/dashboard-contabil.css b/static/css/dashboard-contabil.css
index 19b9596..49e1281 100644
--- a/static/css/dashboard-contabil.css
+++ b/static/css/dashboard-contabil.css
@@ -67,6 +67,121 @@
border-radius: var(--radius-md);
}
+/* Ordenação + filtro "estilo Excel" por coluna do histórico (#dc-list-table)
+ — mesmo mecanismo/classes de .ips-* em importacao-plano-saude.css, só
+ renomeado pro prefixo desta página (não compartilhado entre os dois
+ arquivos porque cada tela carrega só o próprio CSS). */
+#dc-list-table th[data-sort] {
+ cursor: pointer;
+ user-select: none;
+ white-space: nowrap;
+}
+
+.dc-sort-icon {
+ color: var(--text-muted);
+ font-size: 0.75rem;
+}
+
+#dc-list-table th[data-sort].is-sort-asc .dc-sort-icon,
+#dc-list-table th[data-sort].is-sort-desc .dc-sort-icon {
+ color: var(--accent);
+}
+
+/* .pa-table-wrap normalmente usa overflow:hidden pra arredondar os cantos da
+ tabela — dc-list-table-wrap (só esta tabela) sobrescreve pra visible,
+ senão o popup de filtro (que precisa "vazar" pra fora do cabeçalho, sobre
+ as linhas) seria cortado ali. */
+.dc-list-table-wrap {
+ overflow: visible;
+}
+
+.dc-col-filter {
+ position: relative;
+ display: inline-block;
+ margin-left: var(--space-1);
+}
+
+.dc-col-filter__btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ border: none;
+ background: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ border-radius: var(--radius-sm);
+ vertical-align: middle;
+}
+
+.dc-col-filter__btn:hover {
+ color: var(--text-primary);
+ background: var(--bg-canvas);
+}
+
+.dc-col-filter__btn.is-active {
+ color: var(--accent);
+}
+
+/* Botão "borracha" no fim do cabeçalho do histórico — limpa de uma vez todos
+ os filtros por coluna e volta a ordenação pro padrão (mais recente
+ primeiro). */
+.dc-list-reset-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ padding: 0;
+ border: none;
+ background: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ border-radius: var(--radius-sm);
+}
+
+.dc-list-reset-btn:hover {
+ color: var(--text-primary);
+ background: var(--bg-canvas);
+}
+
+.dc-col-filter__popup {
+ position: absolute;
+ top: calc(100% + 6px);
+ left: 0;
+ z-index: 20;
+ width: 220px;
+ padding: var(--space-3);
+ background: var(--bg-surface-raised);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
+ text-align: left;
+ text-transform: none;
+ font-weight: 400;
+ letter-spacing: normal;
+ color: var(--text-primary);
+}
+
+.dc-col-filter__popup .checklist-box {
+ max-height: 220px;
+}
+
+.dc-col-filter__actions {
+ display: flex;
+ justify-content: space-between;
+ gap: var(--space-2);
+ margin-top: var(--space-2);
+}
+
+.dc-col-filter__actions button {
+ flex: 1;
+ padding: var(--space-1) var(--space-2);
+ font-size: 0.8rem;
+}
+
.dc-concluida-aviso {
font-size: 0.85rem;
color: var(--gold);
diff --git a/static/js/dashboard-contabil.js b/static/js/dashboard-contabil.js
index 39dc2ad..7b92ef4 100644
--- a/static/js/dashboard-contabil.js
+++ b/static/js/dashboard-contabil.js
@@ -429,6 +429,18 @@ document.addEventListener("DOMContentLoaded", async () => {
// 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";
@@ -437,14 +449,71 @@ document.addEventListener("DOMContentLoaded", async () => {
// ---- 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() {
- const apuracoes = await pidFetchApuracoesContabil();
+ dcListaApuracoes = await pidFetchApuracoesContabil();
+ renderList();
+ }
+
+ function renderList() {
const body = document.getElementById("dc-list-body");
const empty = document.getElementById("dc-list-empty");
- body.innerHTML = "";
- empty.hidden = apuracoes.length > 0;
- apuracoes.forEach((apuracao) => {
+ const 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 = `
@@ -469,8 +538,197 @@ document.addEventListener("DOMContentLoaded", async () => {
`;
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