portal_publico/portal_api/nao_conformidades/CHANGELOG.md

22 KiB
Raw Blame History

Changelog — Não Conformidades

Histórico específico desta aplicação, extraído de plano.md.

Formato: uma entrada por rodada, ### Rodada N — Título; quando a rodada não tem número registrado, ### Título só. A numeração de rodada não é global — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de plano.md.

Rodada 88 — Não Conformidades (Relatórios > Qualidade)

Pedido: a área de Qualidade tinha uma skill do Claude (analise-ncs, ver projects/Controladoria - Analise NCs/) que gerava, sob demanda, um Excel de 7 abas a partir de dois arquivos exportados do Sigsistem (ocorrências .xlsx + ações .xls, esse último na verdade HTML). O usuário pediu uma aplicação dentro do Portal que evoluísse essa skill pra uma gestão contínua: identificar com facilidade novas ocorrências sem análise (cobrança), análises novas sem ação aberta, acompanhamentos de ação recentes ainda sem tratativa da Qualidade, e priorizar ações vencidas/vencendo em 7 dias — com a possibilidade de marcar manualmente o que já foi tratado, e um dashboard quantitativo (ações abertas, motivos de abertura, clientes/colaboradores com maior incidência).

Decisões de negócio confirmadas com o usuário antes de implementar:

  • O status de "tratado" pela Qualidade é um controle interno do Portal, sem escrita de volta no Sigsistem (sistema externo, sem integração) — mas precisa reabrir sozinho quando uma importação nova trouxer algo diferente do que existia no momento em que foi tratado (ex.: novo acompanhamento numa ação já marcada tratada).
  • Guardar o histórico completo de acompanhamentos de cada ação, não só o último (diferente da skill original).
  • "Colaborador com maior incidência" no dashboard = coluna Indicado para Descrever Análise da Ocorrência; "motivo de abertura" = Tipo(s) de Causa(s) da Ocorrência (não Tipo de Ocorrência).
  • Permissão de toggle único (mesmo padrão de Indicador de Desempenho/Importação de Plano de Saúde), restrita por padrão — só "Integração e Inovação" nasce com acesso, diferente de "Relatório Setorial" (que herda automático via SECTORAL_KEYS).

O que foi construído: pacote Python puro portal_api/nao_conformidades/ (parsing sem pandas, mesmo padrão de planos_saude/indicadores — validado que o volume real, ~236 ocorrências/290 ações/929 acompanhamentos, não justifica introduzir pandas como dependência direta), portando a lógica de referência do script gerar_relatorio.py da skill (inclusive o parsing por regex do .xls-que-é-HTML, com uma diferença: aqui se guarda TODAS as entradas de acompanhamento, não só a última). 4 models novos (NaoConformidadeImportacao, NCOcorrencia, NCAcao, NCAcompanhamento, migrações 0052/0053), 3 ModelViewSet + 1 function view de dashboard, subgrupo qualidade novo em catalogo.MODULE_APPS["relatorios"] (mesmo formato de subgrupo-com-uma-tool já usado em Auditorias), override em seed_portal.py pra restringir o acesso por padrão, e uma tela nova com 3 abas (Gestão/Dashboard/Importar).

Núcleo técnico — reabertura automática por diff (nao_conformidades/diff.py): cada NCOcorrencia/NCAcao guarda um snapshot_tratativa (congelado no momento em que a Qualidade marca como tratada); a cada importação nova, se o item já estava tratado, o snapshot congelado é comparado contra o estado recém-extraído — ação reabre por comparação de data do último acompanhamento (robusto contra reformatação cosmética do export); ocorrência reabre por hash de texto da análise (normalizado) ou por uma ação nova ter sido aberta. Validado contra a amostra real (projects/Controladoria - Analise NCs/ocorrencias_pa_27082026.xlsx/acoes_27082026.xls) via chamada direta da função de diff simulando um acompanhamento mais recente — reproduziu o motivo textual esperado.

Validação end-to-end com dados reais: subiu o servidor local, logou como gabriel, fez upload dos dois arquivos de amostra pela API — os totais bateram com o relatório de referência da skill (Relatorio_Qualidade_20260827.xlsx) em todos os indicadores (236 ocorrências, 13 sem análise, 8 análise sem ação, 38 NCs sem ação corretiva, 2 ações vencidas, 130 vencendo em ≤7 dias), exceto "Total de Ações Abertas" (290 aqui vs. 311 na skill) — a skill original contava também linhas de ocorrência sem nenhuma ação de fato aberta (artefato do script de referência, todas = v.copy() sem filtrar por Código da Ação presente); esta ferramenta conta só ações reais, decisão deliberada de manter (documentada em CLAUDE.md). Reimportar o mesmo arquivo é idempotente (0 novos, tudo "atualizado", 0 acompanhamentos duplicados via hash). Endpoints de marcar-tratado/reabrir (ocorrência e ação) testados via API.

Dois bugs de robustez corrigidos durante a validação com dados reais: (1) assunto estourava CharField(max_length=255) — um export real trouxe um valor com 395 caracteres (texto livre do Sigsistem, sem limite aparente); virou TextField, migração 0053. (2) o serializer de ocorrência tentava ler obj.sem_analise (propriedade que só existe no dataclass OcorrenciaExtraida, não no model Django NCOcorrencia) — corrigido pra computar not obj.descricao_analise.strip() direto no SerializerMethodField.

Não testado nesta rodada: a interação real em navegador (não havia ferramenta de automação de browser disponível ainda neste ambiente) — a tela foi construída seguindo rigorosamente os padrões visuais/estruturais já validados de importacao-plano-saude.html/indicador-desempenho.html, o parsing/pipeline/API foram validados de ponta a ponta com dados reais via chamadas HTTP diretas, e o HTML renderizado pelo Django foi conferido — mas o fluxo de clique/preenchimento na tela em si ainda precisava de uma primeira conferência visual. Resolvido na rodada seguinte (ver abaixo).

Rodada 89 — Redesign de Dashboard e Gestão, a partir de teste real na tela

Depois de testar a tela construída na rodada 88, o usuário deu uma lista concreta de feedback de UX/design. Mudanças:

Dashboard:

  • Vira a aba padrão ao abrir a tela (era Gestão) — dá o resumo geral primeiro.
  • Cards ("Ações vencidas", "Vence em ≤7 dias", "Ocorrências sem análise", "Análises sem ação") viram clicáveis: levam pra Gestão já com o filtro/seção correspondente aplicado (drill-down). "Ocorrências abertas"/"Ações abertas" (totais) e "NCs sem ação corretiva" continuam só informativos — decisão do usuário, sem lista equivalente pra este último.
  • Linhas dos 3 rankings (Motivos/Clientes/Colaboradores) também ficam clicáveis: preenchem a busca da Gestão (ver abaixo) com aquele valor.
  • Rankings de Clientes/Colaboradores passam a mostrar 3 contagens (NC vermelho, Reclamação de Cliente dourado, Outros neutro) + um total, em vez de um número só — decisão explícita do usuário depois de perguntado se devia restringir o filtro a NC/Reclamação (como "Clientes" já fazia) ou mostrar tudo com clareza visual: "pode trazer tudo nos dois casos... tratar com cores o tipo de ocorrência e depois um totalizador". Um botão ?/tooltip + legenda (3 bolinhas coloridas) explicam o critério. Endpoint /dashboard/ mudou o formato de clientes_maior_incidencia/colaboradores_maior_incidencia de {..., quantidade} pra {..., nc, reclamacao, outros, total} (Count condicional por tipo na mesma .values().annotate()); o filtro tipo_ocorrencia__in=TIPOS_NC que só "Clientes" tinha antes foi removido (os dois rankings agora agrupam todos os tipos).
  • Dois KPIs de tempo médio novos, não interativos, sobre todo o histórico (sem filtro de período): "Preenchimento da análise" (Avg(data_analise - data_emissao) em NCOcorrencia) e "Abertura da ação" (Avg(acao.data_emissao - ocorrencia.data_analise) em NCAcao), via ExpressionWrapper+DurationField+Avg. Validado contra a amostra real: ~2,7 e ~18,7 dias. Sem um terceiro KPI de "tempo médio de resolução" — checado antes de implementar: Data de Finalização/Dias para Finalização estão 100% em branco no export real (confirma o que já estava documentado — todas as ações exportadas estão em aberto), e o usuário confirmou explicitamente pra não criar um card pra um dado que não existe ("pode não fazer o card, pois não temos esta informação").
  • Seletor de período virou uma fileira de chips (.ncf-chip/.ncf-chip-row, mesmo padrão visual de .ind-departamento-chip de indicador-desempenho.css, duplicado aqui porque essa folha não é carregada nesta página) — antes era um <select> nativo que destoava do resto do Portal.

Gestão:

  • Cada linha das 3 listas (Sem Análise/Análise sem Ação/Ações) ganhou um botão de expandir (.ncf-expand-btn, chevron que gira 90°, mesmo padrão de .cc-painel-fiscal__toggle de custo-contratacao.css/.js) que revela um painel abaixo da linha (.ncf-detail-row), lazy — só busca GET /ocorrencias/{id}/ou/acoes/{id}/ na primeira vez que é aberta.
    • Ocorrência: mostra tudo que faltava na linha (relato completo, descrição da análise completa, cliente(s), pessoas/fornecedores relacionados, riscos relacionados, origem, prazo pra finalizar, ações vinculadas) — resolve os dois pedidos separados do usuário ("ver qual é aquela ocorrência" e "ver o texto todo da análise") com um único mecanismo, já que a maioria dos campos (causa, análise, cliente) já vem na própria listagem e só o resto (relato, origem etc.) precisa do fetch. NCOcorrenciaDetailSerializer ganhou os campos que faltavam (origem, fornecedores_relacionados, riscos_relacionados, data_relato, representante_gerente, prazo_finalizar); NCOcorrenciaSerializer (base, usado pelas listagens) ganhou emissor_relato (fixando uma coluna "Emissor" que sempre mostrava "—" desde a rodada 88, por não estar no serializer usado pela lista).
    • Ação: removido o botão "Histórico" separado (ficava colado no "Marcar como tratado") e o modal #ncf-acompanhamentos-modal — agora é o mesmo painel de expandir, mostrando o texto completo da ação + o histórico de acompanhamentos ordenado do mais recente pro mais antigo, 3 primeiros visíveis com "Mostrar mais (N)" revelando o resto (tudo client-side, a partir do GET /acoes/{id}/ que já devolve a lista completa).
  • A coluna única "Status" das Ações (que misturava vencimento e tratativa, confuso — usuário perguntou "qual o critério de separação") virou duas colunas: Prazo (Vencida/Vence em ≤7/≤30 dias/No prazo/Sem vencimento) e Tratativa (Pendente/Tratado + selo Reaberto). O filtro virou dois grupos de chips combináveis (Prazo × Tratativa) em vez de uma lista só. Backend: NCAcaoViewSet trocou ?vencidas=/?vence_em= por um único ?status_prazo=<bucket> (_filtra_status_prazo, replica os cortes de classificacao.status_prazo() como filtro de data sobre vencimento_efetivo).
  • Campo de busca único no topo da Gestão (#ncf-gestao-search) filtra as 3 listas ao mesmo tempo, client-side (sobre os dados já carregados, sem round-trip — volume atual não justifica), por colaborador/responsável/emissor/cliente/assunto. NCAcaoSerializer ganhou ocorrencia_clientes/ocorrencia_area (campos só-leitura via source="ocorrencia.…") pra permitir buscar/exibir Ações por empresa — o segundo também corrigiu a coluna "Área" da tabela de Ações, que sempre mostrava "—" desde a rodada 88 (NCAcao não tem campo próprio de área, é da ocorrência).

Bug de corrida corrigido durante o teste: ativarTab() tinha um efeito colateral (carregava a Gestão sozinha ao trocar de aba); os drill-downs do Dashboard chamavam ativarTab("gestao") E também recarregavam explicitamente com o filtro escolhido — as duas chamadas concorrentes escreviam na mesma lista em memória, e a que terminasse por último vencia (às vezes a errada, mostrando "Todas" em vez do filtro clicado). Corrigido tornando ativarTab() puramente de UI (nunca dispara carregamento) — só quem clica explicitamente num botão de aba ou numa função de drill-down decide carregar, sempre com await, nunca em paralelo com outra chamada à mesma lista. Também corrigido um bug de CSS: o painel de detalhe (única <td> da linha, portanto sempre "a última") herdava text-align:right de .pa-table td:last-child, deixando o texto do painel alinhado à direita — resolvido com uma classe própria de maior especificidade (.pa-table td.ncf-detail-cell).

Validado com Playwright (instalado ad-hoc neste ambiente pra esta rodada — npx playwright install chromium + um script Node local, não é dependência do projeto): login, drill-down dos 4 cards, clique numa linha de ranking, expandir ocorrência e ação, alternar tratativa (marcar/reabrir) com o filtro "Tratadas" refletindo a mudança, filtro combinado Prazo+Tratativa+busca — sem erros de console/rede além do 401 esperado da checagem de sessão antes do login.

Rodada 90 — Tempos médios — terceira etapa (execução da ação) e ciclo completo

Na rodada 89, o terceiro KPI de tempo médio (resolução de ações) tinha sido descartado porque Data de Finalização/Dias para Finalização estavam 100% vazios no export padrão do Sigsistem (só traz ações em aberto). O usuário trouxe um export diferente — "com finalizadas" (ocorrencias_pa_28082026-01 - COM FINALZIADAS.xlsx + acoes_28082026-01 - com finalizadas.xls, em C:\Users\Depaula\Documents\Projetos\Não Conformidades) — que traz o histórico de ações já concluídas: 3474 linhas (vs. 287 do export normal), com Data de Finalização preenchida em 3187 delas.

Antes de implementar, conferido que o Dias para Finalização calculado pelo Sigsistem bate exatamente com data_finalizacao - data_emissao (0 divergências em 3187 casos) e que a média geral (~62 dias) esconde uma variância enorme (1 a ~793 dias) entre tipos de ação bem diferentes.

  • tempos_medios do dashboard ganhou uma 3ª etapa (execucao_acao_dias/n, abertura da ação → finalização, só sobre NCAcao com data_finalizacao preenchida — sem filtrar por situacao, já que "Eficaz" e "Ineficaz" indicam igualmente que a ação foi concluída), um KPI de ciclo completo (ciclo_completo_dias/n, emissão da ocorrência → finalização da ação) e uma quebra por categoria (execucao_por_categoria, via classificacao.categoria_acao(), calculada em Python porque é uma heurística de texto). As duas etapas que já existiam (preenchimento da análise, abertura da ação) ganharam n junto da média, pra deixar claro o tamanho da amostra por trás de cada número.
  • Frontend: primeira versão trocou os cards por um funil visual (Emissão → Análise → Ação aberta → Ação finalizada, setas com a média de dias/n entre etapas) — revertido no mesmo dia a pedido do usuário ("os tempos médios, deixe como estava anteriormente, só inclua as informações do tempo para conclusão"): .ncf-kpis/.ncf-kpi continuam o card grid original de 2 cards, agora com 4 (análise, abertura, execução, ciclo completo), cada um mostrando n como uma segunda linha pequena abaixo do valor. A quebra por categoria (execucao_por_categoria) continua como tabelinha (.pa-table) abaixo dos cards.
  • Ver portal_api/nao_conformidades/CLAUDE.md, seção "Tempos médios", pro detalhamento técnico.

Rodada 91 — Upload travando com "Erro ao processar a solicitação." — upsert virou lote

Usuário reportou erro genérico ao tentar reprocessar os mesmos dois arquivos "com finalizadas" da rodada 90 (ocorrencias_pa_28082026-01 - COM FINALZIADAS.xlsx + acoes_28082026-01 - com finalizadas.xls) e perguntou se seria a extensão .xls/.xlsx ou uma incompatibilidade com o ambiente Linux de produção. Nenhum dos dois: reproduzido localmente com os arquivos reais, o upload funcionava, só que levava ~60s — tempo o bastante pra estourar o timeout de um worker WSGI (gunicorn, 30s por padrão) em produção, ou disparar o autoreload do manage.py runserver em dev, derrubando a conexão no meio sem nenhum erro de dado por trás.

Causa raiz: _aplica_upsert_nao_conformidades (rodada 88) fazia update_or_create/get_or_create um item por vez — aceitável nos ~300 registros da amostra inicial (~2 idas ao banco por item), mas o export real de "com finalizadas" tem ~1700 ocorrências/~3400 ações/~9000 acompanhamentos — quase 28 mil idas ao banco. Reescrito pra lote: pré-carrega o que já existe (poucos SELECT ... WHERE codigo IN (...)) e aplica tudo de uma vez (bulk_create/bulk_update por model, batch_size=500) — e, mais importante pro caso real de reimportação periódica, pula por completo quem não mudou nada desde a última importação (reimportar o Sigsistem inteiro todo mês traz de volta o histórico completo, não só o que é novo). Resultado medido com os arquivos reais: primeira carga ~21s (a maioria virando "atualizada" pela primeira vez), reimportação do mesmo arquivo sem nenhuma mudança real caiu de ~60s pra ~4s (a maior parte disso é upload do arquivo de 23MB + parsing, não mais banco).

Efeito colateral documentado (não é regressão, é a semântica correta): resumo["ocorrencias_atualizadas"]/["acoes_atualizadas"] agora contam só quem teve algum campo realmente alterado, não "já existia e foi vista de novo". Nenhuma migração, nenhuma mudança de contrato de API — só a implementação interna do upsert. Detalhe técnico em portal_api/nao_conformidades/CLAUDE.md, seção "Upsert".

Rodada 107 — "Não foi possível copiar o texto." no botão Copiar do Resumo (produção HTTP)

Usuário reportou o aviso "Não foi possível copiar o texto." ao clicar em "Copiar" no popover do Resumo de Gestão, em produção. Causa: navigator.clipboard.writeText() (única chamada à Clipboard API no Portal) só funciona em contexto seguro (HTTPS ou localhost) — a produção deste Portal roda em HTTP puro hoje, sem TLS, e não há previsão de migrar pra HTTPS no momento (decisão de infraestrutura fora do escopo desta aplicação).

Correção: resumoCopiarBtn agora chama pidNcfCopiarTexto(), que usa navigator.clipboard.writeText() quando window.isSecureContext é verdadeiro e, senão, cai num fallback com document.execCommand("copy") via um <textarea> temporário fora da tela (pidNcfCopiarTextoFallback). Sem migração, sem mudança de contrato de API — só o handler de clique em nao-conformidades.js. Detalhe em portal_api/nao_conformidades/CLAUDE.md, seção "Frontend".

Rodada 108 — Melhorias da revisão de interface de 30/09/2026 (2026-10-01)

Pedido do usuário: aplicar os itens da revisão de interface de 30/09/2026 nesta tela (versão sem acessibilidade de teclado/leitor de tela, decisão do usuário). Feito em duas etapas no mesmo pedido:

Primeira etapa (30/09):

  • O Resumo copiável mostra "Finalizada em" com data_finalizacao (antes usava a data de vencimento).
  • A busca da Gestão também procura em tipos de causa (tipos_causa nas ocorrências, ocorrencia_tipos_causa nas ações). Para isso o NCAcaoSerializer ganhou o campo somente leitura ocorrencia_tipos_causa (source="ocorrencia.tipos_causa"), sem migração.
  • Rolar dentro de um popover (lista de tipos do filtro de coluna, Resumo longo) não o fecha mais; antes o fim da lista ficava inalcançável.

Segunda etapa (01/10), só frontend (nao-conformidades.js/.html/.css):

  • Resposta antiga não vence mais: cada carga (Sem análise, Análise sem ação, Ações, Dashboard) tem um contador; só a mais recente escreve na tela. Antes, cliques rápidos nos chips de Prazo, Tratativa ou Período deixavam a resposta que chegasse por último vencer, mesmo sendo do filtro anterior.
  • Falha não trava a aba: gestaoCarregada/importarCarregado só viram verdadeiros depois de carregar com sucesso (antes viravam antes da carga e, se ela falhasse, a aba nunca mais recarregava). Erro de carga aparece no lugar da mensagem de lista vazia ("Não foi possível carregar. Recarregue a página; se persistir, contate a Inovação."), inclusive na carga inicial do Dashboard e no histórico de importações.
  • Sem envio duplo: "Marcar como tratado"/"Reabrir" e "Excluir" importação ficam travados ("Marcando…", "Reabrindo…", "Excluindo…") até a resposta.
  • Linhas abertas continuam abertas depois de marcar como tratado, ordenar ou buscar (ids guardados por tabela e reabertos no redesenho). Falha ao carregar o detalhe de uma linha diz o próximo passo ("Feche e abra a linha de novo; se persistir, recarregue a página.") e reabrir a linha tenta de novo.
  • Busca espera ~200 ms sem digitação antes de redesenhar as 3 tabelas (antes redesenhava a cada tecla).
  • Importação: se o upload deu certo e só a atualização da tela falhou, a mensagem é "Importação concluída, mas não foi possível atualizar a tela. Recarregue a página.", não uma falha de upload.
  • Acabamento: dias médios formatados com Intl.NumberFormat("pt-BR"), reticências "…" nos textos de espera e na busca (com spellcheck="false" e name), color-scheme nos campos de data do filtro de Vencimento seguindo o tema, overflow-wrap: anywhere nos textos longos do detalhe e do histórico, tabular-nums em cards, KPIs e rankings, nome de arquivo longo com reticências, overscroll-behavior: contain no popover do Resumo e touch-action: manipulation em linhas/cards clicáveis e no botão de expandir.

Não feito (outra rodada, por decisão do pedido): estado da tela na URL e virtualização das listas.