Revisão do formato dos número - Relatório Contábil
This commit is contained in:
parent
39ea801899
commit
aab55ec16e
@ -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_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\" \"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/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)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
### 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.
|
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 `<th>`, 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.
|
||||||
|
|||||||
@ -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.
|
**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 `<tr>` — 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 `<th>`): `empresa` (por `codigo_empresa`), `competencia`, `observacoes` (`total_achados_pendentes`), `status`, `criado_por` (`criado_por_nome`), `criado_em`.
|
||||||
|
- **Colunas com filtro "estilo Excel"** (funil no `<th>`, 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 `<th>` 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.
|
**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.
|
||||||
|
|||||||
@ -56,6 +56,19 @@ VARIACAO_VALOR_MINIMO = Decimal("1000") # ignora variações abaixo disso, mesm
|
|||||||
VARIACAO_AV_PONTOS_PERCENTUAIS_MINIMO = Decimal("1")
|
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]:
|
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)
|
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)
|
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,
|
severidade=SEVERIDADE_ALTA,
|
||||||
titulo="Ativo não bate com Passivo",
|
titulo="Ativo não bate com Passivo",
|
||||||
mensagem=(
|
mensagem=(
|
||||||
f"Saldo do Ativo (R$ {ativo.saldo_atual}) não coincide com o do Passivo "
|
f"Saldo do Ativo ({_moeda(ativo.saldo_atual)}) não coincide com o do Passivo "
|
||||||
f"(R$ {-passivo.saldo_atual}) — diferença de R$ {abs(diferenca)}."
|
f"({_moeda(-passivo.saldo_atual)}) — diferença de {_moeda(abs(diferenca))}."
|
||||||
),
|
),
|
||||||
valor_referencia=diferenca,
|
valor_referencia=diferenca,
|
||||||
)
|
)
|
||||||
@ -91,8 +104,8 @@ def regra_debito_credito_divergente(atual: ResultadoExtracao, historico: list[Sn
|
|||||||
severidade=SEVERIDADE_ALTA,
|
severidade=SEVERIDADE_ALTA,
|
||||||
titulo="Débito total diferente do crédito total",
|
titulo="Débito total diferente do crédito total",
|
||||||
mensagem=(
|
mensagem=(
|
||||||
f"Soma de débitos do período (R$ {soma_debito}) não bate com a soma de "
|
f"Soma de débitos do período ({_moeda(soma_debito)}) não bate com a soma de "
|
||||||
f"créditos (R$ {soma_credito}) — diferença de R$ {abs(diferenca)}."
|
f"créditos ({_moeda(soma_credito)}) — diferença de {_moeda(abs(diferenca))}."
|
||||||
),
|
),
|
||||||
valor_referencia=diferenca,
|
valor_referencia=diferenca,
|
||||||
)
|
)
|
||||||
@ -108,7 +121,7 @@ def regra_saldo_negativo_caixa(atual: ResultadoExtracao, historico: list[Snapsho
|
|||||||
regra="saldo_negativo_caixa",
|
regra="saldo_negativo_caixa",
|
||||||
severidade=SEVERIDADE_ALTA,
|
severidade=SEVERIDADE_ALTA,
|
||||||
titulo="Saldo de caixa negativo",
|
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,
|
codigo_conta=conta.codigo,
|
||||||
valor_referencia=conta.saldo_atual,
|
valor_referencia=conta.saldo_atual,
|
||||||
)
|
)
|
||||||
@ -127,7 +140,7 @@ def regra_conta_transitoria_com_saldo(atual: ResultadoExtracao, historico: list[
|
|||||||
regra="conta_transitoria_com_saldo",
|
regra="conta_transitoria_com_saldo",
|
||||||
severidade=SEVERIDADE_MEDIA,
|
severidade=SEVERIDADE_MEDIA,
|
||||||
titulo="Conta transitória com saldo",
|
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,
|
codigo_conta=conta.codigo,
|
||||||
valor_referencia=conta.saldo_atual,
|
valor_referencia=conta.saldo_atual,
|
||||||
)
|
)
|
||||||
@ -147,7 +160,7 @@ def regra_conta_deveria_zerar(atual: ResultadoExtracao, historico: list[Snapshot
|
|||||||
regra="conta_deveria_zerar",
|
regra="conta_deveria_zerar",
|
||||||
severidade=SEVERIDADE_MEDIA,
|
severidade=SEVERIDADE_MEDIA,
|
||||||
titulo="Conta que deveria estar zerada",
|
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,
|
codigo_conta=conta.codigo,
|
||||||
valor_referencia=conta.saldo_atual,
|
valor_referencia=conta.saldo_atual,
|
||||||
)
|
)
|
||||||
@ -170,7 +183,7 @@ def regra_saldo_sinal_invertido(atual: ResultadoExtracao, historico: list[Snapsh
|
|||||||
regra="saldo_sinal_invertido",
|
regra="saldo_sinal_invertido",
|
||||||
severidade=SEVERIDADE_MEDIA,
|
severidade=SEVERIDADE_MEDIA,
|
||||||
titulo="Conta do Ativo com saldo credor",
|
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,
|
codigo_conta=conta.codigo,
|
||||||
valor_referencia=conta.saldo_atual,
|
valor_referencia=conta.saldo_atual,
|
||||||
)
|
)
|
||||||
@ -181,7 +194,7 @@ def regra_saldo_sinal_invertido(atual: ResultadoExtracao, historico: list[Snapsh
|
|||||||
regra="saldo_sinal_invertido",
|
regra="saldo_sinal_invertido",
|
||||||
severidade=SEVERIDADE_MEDIA,
|
severidade=SEVERIDADE_MEDIA,
|
||||||
titulo="Conta do Passivo com saldo devedor",
|
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,
|
codigo_conta=conta.codigo,
|
||||||
valor_referencia=conta.saldo_atual,
|
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",
|
titulo="Lucro do balancete diferente do lucro da DRE",
|
||||||
mensagem=(
|
mensagem=(
|
||||||
f'Resultado do exercício no Balancete ("{conta_lucro.descricao}", {conta_lucro.codigo}) é de '
|
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,
|
codigo_conta=conta_lucro.codigo,
|
||||||
valor_referencia=diferenca,
|
valor_referencia=diferenca,
|
||||||
@ -227,7 +240,7 @@ def regra_descricao_generica(atual: ResultadoExtracao, historico: list[SnapshotH
|
|||||||
regra="descricao_generica",
|
regra="descricao_generica",
|
||||||
severidade=SEVERIDADE_BAIXA,
|
severidade=SEVERIDADE_BAIXA,
|
||||||
titulo='Conta com descrição genérica ("DIVERSOS")',
|
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,
|
codigo_conta=conta.codigo,
|
||||||
valor_referencia=conta.saldo_atual,
|
valor_referencia=conta.saldo_atual,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -67,6 +67,121 @@
|
|||||||
border-radius: var(--radius-md);
|
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 {
|
.dc-concluida-aviso {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--gold);
|
color: var(--gold);
|
||||||
|
|||||||
@ -429,6 +429,18 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
// a apuração, ver renderDashboardTab), null até lá.
|
// a apuração, ver renderDashboardTab), null até lá.
|
||||||
let dcIndicadoresAtual = null;
|
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) {
|
function showView(view) {
|
||||||
listView.hidden = view !== "list";
|
listView.hidden = view !== "list";
|
||||||
formView.hidden = view !== "form";
|
formView.hidden = view !== "form";
|
||||||
@ -437,14 +449,71 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
// ---- Histórico ----
|
// ---- 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() {
|
async function carregarLista() {
|
||||||
const apuracoes = await pidFetchApuracoesContabil();
|
dcListaApuracoes = await pidFetchApuracoesContabil();
|
||||||
|
renderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList() {
|
||||||
const body = document.getElementById("dc-list-body");
|
const body = document.getElementById("dc-list-body");
|
||||||
const empty = document.getElementById("dc-list-empty");
|
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");
|
const tr = document.createElement("tr");
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>${pidDcEscapeHtml(apuracao.codigo_empresa)} - ${pidDcEscapeHtml(apuracao.nome_empresa)} ${pidDcFontePdfAtipicaBadgeHtml(apuracao.fonte_pdf_atipica)}</td>
|
<td>${pidDcEscapeHtml(apuracao.codigo_empresa)} - ${pidDcEscapeHtml(apuracao.nome_empresa)} ${pidDcFontePdfAtipicaBadgeHtml(apuracao.fonte_pdf_atipica)}</td>
|
||||||
@ -469,8 +538,197 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
`;
|
`;
|
||||||
body.appendChild(tr);
|
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) => {
|
document.getElementById("dc-list-body").addEventListener("click", async (event) => {
|
||||||
const abrirBtn = event.target.closest("[data-dc-abrir]");
|
const abrirBtn = event.target.closest("[data-dc-abrir]");
|
||||||
if (abrirBtn) {
|
if (abrirBtn) {
|
||||||
|
|||||||
@ -342,17 +342,21 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pa-table-wrap">
|
<div class="pa-table-wrap dc-list-table-wrap">
|
||||||
<table class="pa-table" id="dc-list-table">
|
<table class="pa-table" id="dc-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Empresa</th>
|
<th data-sort="empresa">Empresa <span class="dc-sort-icon">↕</span></th>
|
||||||
<th>Competência</th>
|
<th data-sort="competencia">Competência <span class="dc-sort-icon">↕</span></th>
|
||||||
<th>Observações pendentes</th>
|
<th data-sort="observacoes">Observações pendentes <span class="dc-sort-icon">↕</span></th>
|
||||||
<th>Status</th>
|
<th data-sort="status">Status <span class="dc-sort-icon">↕</span></th>
|
||||||
<th>Criado por</th>
|
<th data-sort="criado_por">Criado por <span class="dc-sort-icon">↕</span></th>
|
||||||
<th>Data</th>
|
<th data-sort="criado_em">Data <span class="dc-sort-icon">↕</span></th>
|
||||||
<th></th>
|
<th>
|
||||||
|
<button type="button" class="dc-list-reset-btn" id="dc-list-reset-btn" title="Limpar filtros e ordenação" aria-label="Limpar filtros e ordenação">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 20H8l-5.5-5.5a2 2 0 0 1 0-2.8l9-9a2 2 0 0 1 2.8 0l6.2 6.2a2 2 0 0 1 0 2.8L13 19" stroke-linejoin="round"/><path d="M8.5 12.5l6 6" stroke-linecap="round"/></svg>
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="dc-list-body"></tbody>
|
<tbody id="dc-list-body"></tbody>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user