Inclusão da opção de excluir a observação equivocada

This commit is contained in:
Gabriel 2026-09-18 14:52:58 -03:00
parent 2a12eabf62
commit bdf35aa814
6 changed files with 91 additions and 8 deletions

View File

@ -90,7 +90,8 @@
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_reprocessamento_log.py\")",
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_recria_achados.py\")",
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_regra_sinal_invertido.py\")",
"Bash(PYTHONPATH='C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal' .venv/Scripts/python.exe -c ' *)"
"Bash(PYTHONPATH='C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal' .venv/Scripts/python.exe -c ' *)",
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:/Users/Depaula/AppData/Local/Temp/claude/c--Users-Depaula-Documents-Portal/cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6/scratchpad/test_excluir_observacao.py\")"
]
}
}

View File

@ -365,3 +365,11 @@ Usuário reportou, com print de achados reais ("Conta do Passivo 'DANITHI LTDA'"
Corrigido com `_indices_descendentes_de_conta_redutora()` (novo, `regras.py`) — identifica toda conta com algum ancestral (não só o pai direto) redutora, usando o mesmo algoritmo de pilha de níveis já usado em toda a aplicação pra árvore de contas (nível = `codigo.count(".")`, ordem de leitura do PDF), não comparação de prefixo de código (o Questor reaproveita código de classificação entre contas analíticas irmãs, então string matching não seria confiável pra achar o pai). Validado com uma árvore sintética reproduzindo o exemplo do usuário (confirmando que o achado deixa de disparar com a correção e **dispararia** sem ela) e rodando contra as 3 apurações reais já em produção — a apuração `1751`/TAROBA (a mesma do print) teve exatamente os 2 achados dos sócios suprimidos.
Não afeta achados já persistidos: os 2 achados reais da apuração `1751` só somem da tela depois de reprocessar essa apuração (o motor de regras roda de novo do zero, ver rodada 143) — nenhuma edição direta no banco foi feita. Detalhe técnico completo no `CLAUDE.md` desta pasta.
### 145. Botão de excluir observação volta — restrito a quem criou, só antes de concluir a análise
Pedido explícito do usuário: "inclua outro botão, transforme o atual em outro e inclua o da lixeira para excluir a observação. A exclusão só poderá ser realizada pelo usuário que criou ela e só pode ser realizada antes do usuário apertar o botão de concluir análise. Após concluir a análise, edição não deve ser permitida." Reverte parte de uma decisão anterior (o botão de excluir tinha sido tirado, deixando só "encerrar" — ocultar das próximas competências, sem apagar) — agora as duas ações convivem: encerrar continua reversível e liberado pra qualquer um do time; excluir é definitivo e só de quem escreveu a observação.
O ícone de "Encerrar" por coincidência já era desenhado como um glifo de lixeira (tampa+alça+corpo) — trocado por um ícone de "arquivo" antes de liberar a lixeira de verdade pro botão novo, senão os dois ficariam visualmente idênticos fazendo coisas diferentes. Backend: `ContabilObservacaoSerializer` ganhou `criado_por` (o id, não só o nome) e `ContabilObservacaoViewSet.perform_destroy()` ganhou a checagem de autoria (`PermissionDenied` se quem pede não é quem criou), além da checagem que já existia de a apuração estar "Em revisão". Frontend: botão novo nos dois lugares onde uma observação aparece (thread inline por conta/linha e as 4 listas de resumo), condicionado a `!concluida && obs.criado_por === me.id`; usa `pidConfirm({perigoso: true})`, nunca `window.confirm()`.
Validado via `Client.force_login()` dentro de uma transação com rollback forçado, contra dados reais em produção: usuário diferente do autor tentando excluir → 403; o próprio autor tentando excluir numa apuração já concluída → 400; o próprio autor excluindo numa apuração em revisão → 204, registro realmente removido. Nada persistido em produção. Detalhe técnico completo no `CLAUDE.md` desta pasta.

View File

@ -462,7 +462,7 @@ Validado de duas formas: (1) ponta a ponta com `Client.force_login()` em transa
`overflow-x: auto` (ajuste anterior) continua como rede de segurança — uma apuração com mais de 3 meses ainda pode precisar de rolagem horizontal, já que não há como garantir que qualquer quantidade de colunas caiba num container de largura fixa só encolhendo texto até um certo ponto (ilegibilidade é o limite). O objetivo desta mudança é só o caso comum (3 meses, o normal do PDF Questor) caber inteiro sem arrastar nada.
**Frontend** (`dashboard-contabil.js`): as observações são carregadas à parte da apuração (`dcCarregarObservacoes()`, chamada ao abrir/criar uma análise) e indexadas por chave natural (`dcObsIndice`), porque não pertencem ao payload da apuração. O editor inline virou uma thread (`dcObsPainelHtml()`/`dcObsItemHtml()`): histórico em cima (autor, data, competência de origem, selos "Histórico"/"Encerrada"/"Editada"/"Aparece ao cliente"/"Interna" e ações de olho, ver histórico de edições (só quando `editada`), editar, encerrar/reativar), campo de observação nova embaixo. **Sem botão de excluir** (removido numa rodada seguinte, pedido explícito do usuário) — a exclusão de uma observação vigente fica limitada ao fluxo de "ocultar das próximas competências" (`encerrar`), nunca um apagar definitivo pela tela; o `DELETE` do backend (`ContabilObservacaoViewSet`, ver "Imutabilidade"/"Endpoints" acima) continua existindo e reachável via API, só não tem mais consumidor no frontend. Um handler único (`dcTrataCliqueObservacao()`) atende as três tabelas e as quatro listas de resumo, e cada mutação refaz o fetch e re-renderiza tudo (`dcRenderObservacoesTudo()`) — a mesma observação pode estar visível em mais de um lugar ao mesmo tempo. O botão da coluna "Observação" ganhou um contador (`.dc-obs-contador`), já que uma conta pode ter várias. Os chips "Todas / Visíveis ao cliente / Internas" (`dcObsFiltro`) existem nas quatro listas e compartilham a mesma variável: filtrar numa aba filtra em todas.
**Frontend** (`dashboard-contabil.js`): as observações são carregadas à parte da apuração (`dcCarregarObservacoes()`, chamada ao abrir/criar uma análise) e indexadas por chave natural (`dcObsIndice`), porque não pertencem ao payload da apuração. O editor inline virou uma thread (`dcObsPainelHtml()`/`dcObsItemHtml()`): histórico em cima (autor, data, competência de origem, selos "Histórico"/"Encerrada"/"Editada"/"Aparece ao cliente"/"Interna" e ações de olho, ver histórico de edições (só quando `editada`), editar, encerrar/reativar, **excluir — voltou na rodada 145, ver abaixo**), campo de observação nova embaixo. A exclusão de uma observação vigente também pode passar pelo fluxo de "ocultar das próximas competências" (`encerrar`), sem apagar nada — as duas opções convivem agora (excluir é definitivo e só de quem criou; encerrar é reversível e qualquer um do time pode usar). Um handler único (`dcTrataCliqueObservacao()`) atende as três tabelas e as quatro listas de resumo, e cada mutação refaz o fetch e re-renderiza tudo (`dcRenderObservacoesTudo()`) — a mesma observação pode estar visível em mais de um lugar ao mesmo tempo. O botão da coluna "Observação" ganhou um contador (`.dc-obs-contador`), já que uma conta pode ter várias. Os chips "Todas / Visíveis ao cliente / Internas" (`dcObsFiltro`) existem nas quatro listas e compartilham a mesma variável: filtrar numa aba filtra em todas.
**Ícone de observação da conta/linha "mãe" também se destaca quando um descendente recolhido tem observação** (mesma rodada do bug acima, pedido explícito do usuário: "caso esteja recolhida, o usuário consegue visualizar se há ou não observações realizadas"): `dcTemObservacaoDescendente(tipo, itens, nivelFn, chaveFn, id)` (nova, reaproveita `dcDescendentes()` — todos os descendentes, não só os filhos diretos) roda pra toda conta/linha sintética (`temFilhos[i]`) nas três árvores (`renderContas()`/`renderDre()`/`renderAnaliseVertical()`) e é passada como quarto argumento de `dcObsBotaoHtml()`. O ícone da própria sintética só ganha o destaque quando ela mesma **não** tem observação própria (senão prevalece o `--preenchida` de sempre) — nova classe `.dc-conta-observacao-btn--descendente` (`dashboard-contabil.css`), cor `--gold` (mesmo tom do estado "parcial" do botão "validado", pra reaproveitar um significado visual já existente de "tem algo pendente de atenção neste grupo" sem inventar uma cor nova). Clicar no ícone continua abrindo o painel da própria conta/linha (que nasce vazio nesse caso) — é só um sinal visual de "tem observação em algum lugar dentro deste grupo recolhido", não um atalho pra ela.
@ -472,6 +472,16 @@ 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.
**Botão de excluir voltou, restrito a quem criou e antes de concluir a análise (rodada 145)**: pedido explícito do usuário — "inclua o da lixeira para excluir a observação. A exclusão só poderá ser realizada pelo usuário que criou ela e só pode ser realizada antes do usuário apertar o botão de concluir análise." Reverte parte da decisão da rodada em que o botão foi tirado (ver "Histórico de edições de texto" acima) — a exclusão de verdade volta a existir, mas com uma trava de autoria que não existia antes de ter sido removida, e sem tirar o "encerrar" (que continua sendo a forma reversível/de qualquer um do time de tirar uma observação das próximas competências).
**Troca de ícone primeiro** (necessária antes de reintroduzir o botão): o ícone de "Encerrar" (`PID_DC_ICON_ENCERRAR`, `dashboard-contabil.js`) por coincidência **era** desenhado como o glifo padrão de lixeira (tampa+alça+corpo) — reaproveitar essa mesma forma pro botão de excluir de verdade criaria dois ícones idênticos com ações bem diferentes (uma reversível, outra não) lado a lado na mesma linha de ações. Trocado por um ícone de "arquivo" (caixa com uma linha), sem nenhuma associação com apagar; `PID_DC_ICON_LIXEIRA` (constante que já existia no arquivo, reaproveitada de `.dc-ind-comp-remover` no construtor de indicador personalizado) passou a ser a lixeira de verdade.
**Backend** (`views.py`): `ContabilObservacaoSerializer` ganhou `criado_por` (o id da FK, além do `criado_por_nome` que já existia) — é o que o frontend compara contra `me.id` pra decidir se mostra o botão. `ContabilObservacaoViewSet.perform_destroy()` ganhou uma segunda checagem, além de `_garante_texto_editavel()` (a mesma regra de "só enquanto Em revisão" que já valia pro texto): `if instance.criado_por_id != self.request.user.id: raise PermissionDenied(...)`. As duas regras juntas cobrem exatamente o pedido — mostrar/ocultar o botão no frontend é só UX, o servidor confere tudo de novo.
**Frontend**: `dcObsItemHtml()` (thread inline por conta/linha) e `dcObsResumoItemHtml()` (as 4 listas de resumo) ganharam o botão (`data-dc-obs-excluir`, `PID_DC_ICON_LIXEIRA`) condicionado a `!concluida && obs.criado_por === me.id` nos dois lugares — a mesma regra duplicada em vez de extraída pra uma função, já que são só duas linhas e cada renderer já tem seu próprio jeito de montar a lista de ações. Novo `pidExcluirObservacaoContabil(id)` (`DELETE /api/contabil-observacoes/{id}/`) e um case novo (`data-dc-obs-excluir`) em `dcTrataCliqueObservacao()` — o handler único já compartilhado pelas 3 tabelas + 4 listas de resumo, então um clique em qualquer um dos 7 lugares onde uma observação pode aparecer funciona igual. Usa `pidConfirm(..., { perigoso: true })` antes de excluir (ação destrutiva, mesmo padrão do resto do app — ver "Modal de confirmação genérico" no `CLAUDE.md` raiz), nunca `window.confirm()`.
Validado via `Client.force_login()` dentro de uma transação com rollback forçado, contra dados reais em produção: (1) um usuário que não criou a observação tenta excluir a de outro → 403; (2) o próprio criador tenta excluir uma observação de uma apuração já **Concluída** → 400 (mesmo `_garante_texto_editavel`); (3) o próprio criador exclui a própria observação numa apuração **Em revisão** → 204, registro realmente some do banco. Nada persistido em produção.
### 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):

View File

@ -2342,7 +2342,11 @@ class ContabilObservacaoSerializer(serializers.ModelSerializer):
`ContabilObservacaoEdicao` (nunca vazio depois de qualquer mudança real
de texto) — usados pro selo "Editada" e pro histórico de edições na
tela; o relatório HTML pro cliente não usa este serializer, então esses
dois campos nunca aparecem lá."""
dois campos nunca aparecem lá. `criado_por` (o id, não só o nome) é o que
o frontend compara contra `me.id` pra decidir se mostra o botão de
excluir — `ContabilObservacaoViewSet.perform_destroy()` faz a mesma
checagem no servidor, então esconder o botão é só UX, nunca a única
barreira."""
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
encerrada_por_nome = serializers.CharField(source="encerrada_por.nome", read_only=True, default=None)
@ -2363,6 +2367,7 @@ class ContabilObservacaoSerializer(serializers.ModelSerializer):
"competencia_origem",
"texto",
"mostrar_ao_cliente",
"criado_por",
"criado_por_nome",
"criado_em",
"encerrada_em_competencia",

View File

@ -4728,10 +4728,17 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
mostrar ou não ao cliente é uma decisão editorial de cada relatório,
inclusive de uma competência posterior. Toda mudança real de `texto`
grava um `ContabilObservacaoEdicao` (texto anterior/novo, autor, data)
— é o que alimenta o selo "Editada" e o histórico de edições na tela,
já que não existe mais botão de excluir a observação.
- `destroy` segue a mesma regra do texto (só apaga o que ainda é da
competência aberta; histórico não se apaga, se encerra).
— é o que alimenta o selo "Editada" e o histórico de edições na tela.
Qualquer usuário com acesso à ferramenta pode editar/mostrar-ocultar
qualquer observação (não só quem criou), enquanto a apuração de origem
estiver em revisão.
- `destroy` **voltou a ter um botão na tela** (rodada 145, revertendo a
decisão de uma rodada anterior de tirar o botão) — mesma regra de
`_garante_texto_editavel` (só apaga o que ainda é da competência
aberta; uma observação histórica nunca se apaga, se encerra) **mais**
uma restrição nova: só quem criou a observação (`criado_por_id ==
request.user.id`) pode excluí-la, senão `PermissionDenied` — diferente
de `partial_update`, que continua liberado pra qualquer um do time.
- `encerrar`/`reativar` ligam/desligam `encerrada_em_competencia` — é o
"ocultar das próximas execuções": continua visível na competência em
que foi encerrada e some a partir da seguinte."""
@ -4818,6 +4825,8 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
def perform_destroy(self, instance: ContabilObservacao) -> None:
self._garante_texto_editavel(instance)
if instance.criado_por_id != self.request.user.id:
raise PermissionDenied("Só quem registrou esta observação pode excluí-la.")
instance.delete()
@action(detail=True, methods=["post"])

View File

@ -281,6 +281,14 @@ async function pidReativarObservacaoContabil(id, apuracaoId) {
return pidApiRequest(`/contabil-observacoes/${id}/reativar/`, { method: "POST", body: { apuracao: apuracaoId } });
}
// Excluir de verdade — botão voltou a existir na tela (rodada 145), restrito
// a quem criou a observação e só enquanto a apuração de origem estiver em
// revisão (mesmas duas regras checadas de novo no servidor, ver
// ContabilObservacaoViewSet.perform_destroy()).
async function pidExcluirObservacaoContabil(id) {
return pidApiRequest(`/contabil-observacoes/${id}/`, { method: "DELETE" });
}
// `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
@ -1819,8 +1827,15 @@ document.addEventListener("DOMContentLoaded", async () => {
</button>`;
}
// Ícone de "Encerrar" trocado nesta rodada — o desenho antigo (tampa+alça+
// corpo) é literalmente o glifo padrão de lixeira, e agora existe um botão
// de excluir de verdade na mesma linha de ações (PID_DC_ICON_LIXEIRA
// abaixo), então os dois não podiam mais compartilhar a mesma forma sem
// confundir "encerrar" (só some das próximas competências, o texto
// continua no histórico) com "excluir" (apaga o registro). Ícone novo é um
// "arquivo" (caixa com uma linha), sem nenhuma associação com apagar.
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>';
'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="5" rx="1"/><path d="M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9"/><path d="M10 13h4"/></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>';
@ -1864,6 +1879,16 @@ document.addEventListener("DOMContentLoaded", async () => {
: `<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>`
);
}
// Excluir de verdade — pedido explícito do usuário: só quem criou pode
// excluir, e só enquanto a apuração ainda está "Em revisão" (depois de
// "Concluir Análise" nem edição nem exclusão são mais permitidas). O
// servidor confere as duas regras de novo (ContabilObservacaoViewSet.
// perform_destroy) — esconder o botão aqui é só UX.
if (!concluida && obs.criado_por === me.id) {
acoes.push(
`<button type="button" class="icon-btn" data-dc-obs-excluir="${obs.id}" title="Excluir esta observação" aria-label="Excluir observação">${PID_DC_ICON_LIXEIRA}</button>`
);
}
const corpo =
dcObsEditandoId === obs.id && podeEditarTexto
@ -2070,6 +2095,25 @@ document.addEventListener("DOMContentLoaded", async () => {
return true;
}
// Excluir de verdade — só aparece pra quem criou a observação e só
// enquanto a apuração ainda está em revisão (ver dcObsItemHtml()/
// dcObsResumoItemHtml(), que já escondem o botão fora dessas condições;
// o servidor confere as duas de novo, então isso aqui é só UX).
const excluirBtn = event.target.closest("[data-dc-obs-excluir]");
if (excluirBtn) {
const id = Number(excluirBtn.getAttribute("data-dc-obs-excluir"));
const ok = await pidConfirm("Excluir esta observação? Essa ação não pode ser desfeita.", { perigoso: true });
if (!ok) return true;
try {
await pidExcluirObservacaoContabil(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;
@ -3450,6 +3494,11 @@ document.addEventListener("DOMContentLoaded", async () => {
: 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>`;
// Mesma regra de dcObsItemHtml() — só quem criou, só antes de concluir.
const acaoExcluir =
!concluida && obs.criado_por === me.id
? `<button type="button" class="icon-btn" data-dc-obs-excluir="${obs.id}" title="Excluir esta observação" aria-label="Excluir observação">${PID_DC_ICON_LIXEIRA}</button>`
: "";
return `
<div class="dc-dash-obs${obs.mostrar_ao_cliente ? "" : " is-oculto"}">
<div class="dc-dash-obs__info">
@ -3471,6 +3520,7 @@ document.addEventListener("DOMContentLoaded", async () => {
${obs.mostrar_ao_cliente ? PID_DC_ICON_OLHO_ABERTO : PID_DC_ICON_OLHO_FECHADO}
</button>
${acaoEncerrar}
${acaoExcluir}
</div>
</div>
`;