18 KiB
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ítulosó. 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 deplano.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ãoTipo 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 declientes_maior_incidencia/colaboradores_maior_incidenciade{..., quantidade}pra{..., nc, reclamacao, outros, total}(Countcondicional por tipo na mesma.values().annotate()); o filtrotipo_ocorrencia__in=TIPOS_NCque 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)emNCOcorrencia) e "Abertura da ação" (Avg(acao.data_emissao - ocorrencia.data_analise)emNCAcao), viaExpressionWrapper+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çãoestã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-chipdeindicador-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__toggledecusto-contratacao.css/.js) que revela um painel abaixo da linha (.ncf-detail-row), lazy — só buscaGET /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.
NCOcorrenciaDetailSerializerganhou os campos que faltavam (origem,fornecedores_relacionados,riscos_relacionados,data_relato,representante_gerente,prazo_finalizar);NCOcorrenciaSerializer(base, usado pelas listagens) ganhouemissor_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 doGET /acoes/{id}/que já devolve a lista completa).
- 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.
- 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:
NCAcaoViewSettrocou?vencidas=/?vence_em=por um único?status_prazo=<bucket>(_filtra_status_prazo, replica os cortes declassificacao.status_prazo()como filtro de data sobrevencimento_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.NCAcaoSerializerganhouocorrencia_clientes/ocorrencia_area(campos só-leitura viasource="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 (NCAcaonã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_mediosdo dashboard ganhou uma 3ª etapa (execucao_acao_dias/n, abertura da ação → finalização, só sobreNCAcaocomdata_finalizacaopreenchida — sem filtrar porsituacao, 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, viaclassificacao.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) ganharamnjunto 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/nentre 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-kpicontinuam o card grid original de 2 cards, agora com 4 (análise, abertura, execução, ciclo completo), cada um mostrandoncomo 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".