Ajuste na aplicação Não Conformidades.

This commit is contained in:
Gabriel 2026-08-28 17:57:52 -03:00
parent 0bac4d41f8
commit 0886141ae3
11 changed files with 247032 additions and 12 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -440,4 +440,24 @@ do Dashboard, expandir ocorrência/ação, marcar tratado/reabrir, filtros
combinados) — ver `portal_api/nao_conformidades/CHANGELOG.md`. Falta só o
upload real do formulário "Nova Importação" pelo navegador (testado até
agora só via API), e a conferência do usuário sobre o resultado visual do
redesign da rodada 89.
redesign da rodada 89 e dos novos cards de tempo médio da rodada 90.
### 90. Não Conformidades — tempo médio de execução da ação e ciclo completo
O usuário trouxe um export do Sigsistem diferente do original ("com
finalizadas" — traz o histórico de ações já concluídas, 3474 linhas contra
287 do export padrão, `Data de Finalização` preenchida em 3187), viabilizando
o terceiro KPI de tempo médio que tinha sido descartado na rodada 89 por
falta de dado real. `tempos_medios` do dashboard ganhou uma 3ª etapa
(execução da ação, abertura → finalização) e um KPI de ciclo completo
(emissão da ocorrência → finalização da ação), cada um com o tamanho da
amostra (`n`) junto da média — inclusive retroativo às 2 etapas que já
existiam. A execução também é quebrada por categoria de ação
(Correção/Ação Corretiva/Outros), porque a média geral (~62 dias) escondia
uma variância enorme (1 a ~793 dias) entre tipos bem diferentes. No
frontend, uma primeira versão trocou os cards por um funil visual com setas
entre etapas — revertida no mesmo dia a pedido do usuário, que preferiu
manter o card grid original (agora com 4 cards em vez de 2, cada um com o
`n` da amostra). Detalhe completo em
`portal_api/nao_conformidades/CHANGELOG.md` (rodada 90) e
`portal_api/nao_conformidades/CLAUDE.md` (seção "Tempos médios").

View File

@ -44,3 +44,13 @@ Depois de testar a tela construída na rodada 88, o usuário deu uma lista concr
**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.
### 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.

View File

@ -70,7 +70,16 @@ Todos os endpoints sob `PermissaoApp("relatorios", "nao-conformidades")` (toggle
| `/api/nao-conformidades/acoes/` | GET | filtros `?status_tratativa=`, `?status_prazo=<bucket>` (`vencida`/`vence_7_dias`/`vence_30_dias`/`no_prazo`/`sem_vencimento`, replica `classificacao.status_prazo()` como filtro de data via `_filtra_status_prazo`), `?search=`; inclui `status_prazo`/`status_prazo_label`/`dias_para_vencer`/`ocorrencia_area`/`ocorrencia_clientes` calculados no serializer (nunca persistidos) |
| `/api/nao-conformidades/acoes/{id}/` | GET | detalhe com `acompanhamentos` completo (histórico) |
| `/api/nao-conformidades/acoes/{id}/marcar-tratado/`, `/reabrir/` | POST | idem ocorrência |
| `/api/nao-conformidades/dashboard/` | GET | `?meses=` (default 6) ou `?data_inicio=&data_fim=`; indicadores "abertos agora" (nunca filtrados por período) + `tempos_medios` (2 KPIs, sobre todo o histórico, nunca filtrados por período) + rankings de motivo (`tipos_causa`)/cliente (`clientes_relacionados`)/colaborador (`indicado_analise`) filtrados pelo período — clientes/colaboradores vêm quebrados por tipo (`{nome, nc, reclamacao, outros, total}`) |
| `/api/nao-conformidades/dashboard/` | GET | `?meses=` (default 6) ou `?data_inicio=&data_fim=`; indicadores "abertos agora" (nunca filtrados por período) + `tempos_medios` (4 KPIs + quebra por categoria, sobre todo o histórico, nunca filtrados por período — ver "Tempos médios" abaixo) + rankings de motivo (`tipos_causa`)/cliente (`clientes_relacionados`)/colaborador (`indicado_analise`) filtrados pelo período — clientes/colaboradores vêm quebrados por tipo (`{nome, nc, reclamacao, outros, total}`) |
### Tempos médios (`nao_conformidades_dashboard_view`, `tempos_medios`)
4 cards (`.ncf-kpi`, mesmo componente visual desde a rodada 89 — decisão explícita do usuário de manter o card simples em vez de um funil visual com setas, que chegou a ser implementado e foi revertido na mesma rodada): preenchimento da análise, abertura da ação, execução da ação (abertura → finalização) e ciclo completo (emissão da ocorrência → finalização da ação) — não é a soma dos 3 anteriores, porque cada etapa tem sua própria amostra (`n`) e nem toda ocorrência percorre todas as etapas até aqui. A etapa de execução (`execucao_acao_dias`/`n`) só existe desde que a planilha de ocorrências passou a trazer ações **já finalizadas** (`Data de Finalização`/`Dias para Finalização`, colunas que a exportação padrão do Sigsistem não preenche — só um export "com finalizadas" as traz, ver histórico do pacote); antes disso só os 2 primeiros KPIs tinham dado.
- Cada média vem acompanhada do **tamanho da amostra** (`*_n`) — sem isso um número como "62 dias" parece mais sólido do que é, já que a amostra de quem já finalizou é normalmente bem menor que o total de ocorrências.
- `execucao_acao_dias`/`n` é calculado só sobre `NCAcao` com `data_finalizacao` **e** `data_emissao` preenchidas (`Avg(data_finalizacao - data_emissao)` — confirmado que bate exatamente com o `Dias para Finalização` que o próprio Sigsistem calcula, então não precisa usar esse campo cru, só as datas). **Não filtra por `situacao`** ("Eficaz"/"Ineficaz") — os dois indicam que a ação foi concluída e avaliada, então os dois contam pro tempo de execução.
- **Quebrado por categoria** (`execucao_por_categoria`, via `classificacao.categoria_acao()`) porque a média geral esconde uma variância enorme — na amostra real (import "com finalizadas", 3187 ações), de 1 a quase 800 dias, média 62. Correção/Ação Corretiva/Outros têm perfis de duração bem diferentes; misturar tudo numa média só seria enganoso.
- Cálculo em Python (`views.py`, não uma `annotate` agrupada por categoria no ORM), porque `categoria_acao()` é uma heurística de texto (`"corretiva" in tipo.lower()` etc.) que não dá pra expressar como `CASE`/`Q` sem duplicar a lógica — o volume real (milhares de ações, não milhões) não justifica a complexidade de fazer isso no banco.
## Frontend (`templates/nao-conformidades.html`, `static/{css,js}/nao-conformidades.js`)

View File

@ -3017,18 +3017,60 @@ def nao_conformidades_dashboard_view(request: Request) -> Response:
tempo_analise = NCOcorrencia.objects.filter(
data_analise__isnull=False, data_emissao__isnull=False
).aggregate(media=Avg(ExpressionWrapper(F("data_analise") - F("data_emissao"), output_field=DurationField())))[
"media"
]
).aggregate(
media=Avg(ExpressionWrapper(F("data_analise") - F("data_emissao"), output_field=DurationField())),
n=Count("id"),
)
tempo_abertura_acao = NCAcao.objects.filter(
data_emissao__isnull=False, ocorrencia__data_analise__isnull=False
).aggregate(
media=Avg(ExpressionWrapper(F("data_emissao") - F("ocorrencia__data_analise"), output_field=DurationField()))
)["media"]
media=Avg(ExpressionWrapper(F("data_emissao") - F("ocorrencia__data_analise"), output_field=DurationField())),
n=Count("id"),
)
tempo_ciclo_completo = NCAcao.objects.filter(
data_finalizacao__isnull=False, ocorrencia__data_emissao__isnull=False
).aggregate(
media=Avg(
ExpressionWrapper(F("data_finalizacao") - F("ocorrencia__data_emissao"), output_field=DurationField())
),
n=Count("id"),
)
def _dias(intervalo) -> float | None:
return round(intervalo.total_seconds() / 86400, 1) if intervalo is not None else None
# Tempo de execução (abertura → finalização da ação) — só sobre ações com
# Data de Finalização preenchida (ver leiaute_ocorrencias.py). Quebrado por
# categoria_acao() em Python (Correção/Ação Corretiva/Outros variam demais
# entre si — de 1 a quase 800 dias na amostra real — pra uma média só fazer
# sentido sozinha).
execucao_dias_por_tipo = NCAcao.objects.filter(
data_finalizacao__isnull=False, data_emissao__isnull=False
).annotate(
dias=ExpressionWrapper(F("data_finalizacao") - F("data_emissao"), output_field=DurationField())
).values_list("tipo_acao", "dias")
todos_dias: list[float] = []
dias_por_categoria: dict[str, list[float]] = {}
for tipo_acao, dias in execucao_dias_por_tipo:
valor = dias.total_seconds() / 86400
todos_dias.append(valor)
dias_por_categoria.setdefault(nc_classificacao.categoria_acao(tipo_acao), []).append(valor)
ordem_categorias = [
nc_classificacao.CATEGORIA_CORRECAO,
nc_classificacao.CATEGORIA_CORRETIVA,
nc_classificacao.CATEGORIA_OUTROS,
]
execucao_por_categoria = []
for categoria in ordem_categorias:
valores = dias_por_categoria.get(categoria)
if not valores:
continue
execucao_por_categoria.append(
{"categoria": categoria, "media_dias": round(sum(valores) / len(valores), 1), "n": len(valores)}
)
return Response(
{
"ocorrencias_abertas": NCOcorrencia.objects.count(),
@ -3058,8 +3100,15 @@ def nao_conformidades_dashboard_view(request: Request) -> Response:
for c in colaboradores
],
"tempos_medios": {
"preenchimento_analise_dias": _dias(tempo_analise),
"abertura_acao_dias": _dias(tempo_abertura_acao),
"preenchimento_analise_dias": _dias(tempo_analise["media"]),
"preenchimento_analise_n": tempo_analise["n"],
"abertura_acao_dias": _dias(tempo_abertura_acao["media"]),
"abertura_acao_n": tempo_abertura_acao["n"],
"execucao_acao_dias": round(sum(todos_dias) / len(todos_dias), 1) if todos_dias else None,
"execucao_acao_n": len(todos_dias),
"execucao_por_categoria": execucao_por_categoria,
"ciclo_completo_dias": _dias(tempo_ciclo_completo["media"]),
"ciclo_completo_n": tempo_ciclo_completo["n"],
},
"periodo": {"data_inicio": data_inicio.isoformat(), "data_fim": data_fim.isoformat()},
}

View File

@ -195,6 +195,23 @@
font-style: italic;
}
.ncf-kpi__n {
display: block;
font-size: 0.72rem;
color: var(--text-muted);
margin-top: 2px;
}
.ncf-execucao-categoria {
margin-top: var(--space-4);
}
.ncf-execucao-categoria__title {
font-size: 0.85rem;
color: var(--text-secondary);
margin: 0 0 var(--space-2);
}
.ncf-dash-legend {
display: flex;
align-items: center;

View File

@ -536,6 +536,7 @@ document.addEventListener("DOMContentLoaded", async () => {
// ---------------- Dashboard ----------------
const dashCards = document.getElementById("ncf-dash-cards");
const dashKpis = document.getElementById("ncf-dash-kpis");
const dashExecucaoCategoria = document.getElementById("ncf-dash-execucao-categoria");
const dashMotivosBody = document.getElementById("ncf-dash-motivos-body");
const dashClientesBody = document.getElementById("ncf-dash-clientes-body");
const dashColaboradoresBody = document.getElementById("ncf-dash-colaboradores-body");
@ -638,8 +639,26 @@ document.addEventListener("DOMContentLoaded", async () => {
const tempos = dados.tempos_medios || {};
const kpis = [
{ rotulo: "Preenchimento da análise (emissão → análise)", dias: pidNcfFormatDias(tempos.preenchimento_analise_dias) },
{ rotulo: "Abertura da ação (análise → ação aberta)", dias: pidNcfFormatDias(tempos.abertura_acao_dias) },
{
rotulo: "Preenchimento da análise (emissão → análise)",
dias: pidNcfFormatDias(tempos.preenchimento_analise_dias),
n: tempos.preenchimento_analise_n,
},
{
rotulo: "Abertura da ação (análise → ação aberta)",
dias: pidNcfFormatDias(tempos.abertura_acao_dias),
n: tempos.abertura_acao_n,
},
{
rotulo: "Execução da ação (ação aberta → ação finalizada)",
dias: pidNcfFormatDias(tempos.execucao_acao_dias),
n: tempos.execucao_acao_n,
},
{
rotulo: "Ciclo completo (emissão da ocorrência → ação finalizada)",
dias: pidNcfFormatDias(tempos.ciclo_completo_dias),
n: tempos.ciclo_completo_n,
},
];
dashKpis.innerHTML = "";
kpis.forEach((kpi) => {
@ -648,11 +667,32 @@ document.addEventListener("DOMContentLoaded", async () => {
const valorHtml =
kpi.dias == null
? '<span class="ncf-kpi__value ncf-kpi__value--vazio">Sem dados suficientes</span>'
: `<span class="ncf-kpi__value">${kpi.dias} dias</span>`;
: `<span class="ncf-kpi__value">${kpi.dias} dias</span><span class="ncf-kpi__n">n=${kpi.n}</span>`;
el.innerHTML = `<span class="ncf-kpi__label">${kpi.rotulo}</span>${valorHtml}`;
dashKpis.appendChild(el);
});
const execucaoPorCategoria = tempos.execucao_por_categoria || [];
if (!execucaoPorCategoria.length) {
dashExecucaoCategoria.innerHTML = "";
} else {
const linhas = execucaoPorCategoria
.map(
(item) =>
`<tr><td>${pidNcfEscapeHtml(item.categoria)}</td><td>${pidNcfFormatDias(item.media_dias)} dias</td><td>${item.n}</td></tr>`
)
.join("");
dashExecucaoCategoria.innerHTML = `
<h4 class="ncf-execucao-categoria__title">Execução por categoria de ação</h4>
<div class="pa-table-wrap">
<table class="pa-table">
<thead><tr><th>Categoria</th><th>Média</th><th>Qtd.</th></tr></thead>
<tbody>${linhas}</tbody>
</table>
</div>
`;
}
preencheRankingMotivos(dashMotivosBody, dados.motivos_abertura || []);
preencheRankingTipo(dashClientesBody, dados.clientes_maior_incidencia || [], "cliente");
preencheRankingTipo(dashColaboradoresBody, dados.colaboradores_maior_incidencia || [], "colaborador");

View File

@ -329,6 +329,7 @@
<div class="ncf-kpis-section">
<h3 class="ncf-kpis-title">Tempos médios</h3>
<div class="ncf-kpis" id="ncf-dash-kpis"></div>
<div class="ncf-execucao-categoria" id="ncf-dash-execucao-categoria"></div>
</div>
<div class="ncf-dashboard-toolbar">