Compare commits

..

No commits in common. "3cf50872abc81b0f20998c190cbdf821728901bd" and "aa417661f05d2de93db50cb7e047906a26a37824" have entirely different histories.

14 changed files with 317 additions and 126925 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +0,0 @@
# Generated by Django 6.0.7 on 2026-09-08 19:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0054_importacaoplanosaudedepaula_and_more'),
]
operations = [
migrations.AddField(
model_name='ncocorrencia',
name='data_finalizacao',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='ncocorrencia',
name='fase',
field=models.CharField(blank=True, max_length=100, verbose_name='Fase (Sigsistem)'),
),
]

View File

@ -1824,16 +1824,6 @@ class NCOcorrencia(models.Model):
# `acoes.count()` porque uma ocorrência pode ter linhas mistas.
analise_sem_acao_detectada = models.BooleanField(default=False)
# Só relevantes/preenchidos quando a ocorrência não tem nenhuma ação
# (linha do export sem "Código da Ação") e o Sigsistem fecha a própria
# ocorrência direto — ex.: tipo "Elogios de Clientes", que não exige
# análise/ação nenhuma. Vêm das MESMAS colunas "Data de Finalização"/
# "Fase" que, numa linha COM ação, descrevem a ação (ver
# NCAcao.data_finalizacao/fase) — o Sigsistem reaproveita as colunas
# conforme o que a linha representa; ver leiaute_ocorrencias.py.
data_finalizacao = models.DateField(null=True, blank=True)
fase = models.CharField("Fase (Sigsistem)", max_length=100, blank=True)
status_tratativa = models.CharField(
"Status de tratativa", max_length=10, choices=STATUS_CHOICES, default=STATUS_PENDENTE
)

View File

@ -35,11 +35,6 @@ STATUS_PRAZO_LABELS = {
STATUS_SEM_VENCIMENTO: "Sem vencimento",
}
# Valor de "Fase" (coluna do Sigsistem, texto livre) que indica que o item
# está fechado — usado tanto pra NCAcao.fase quanto pra NCOcorrencia.fase
# (ocorrência fechada sem precisar de ação, ex.: "Elogios de Clientes").
FASE_FINALIZADA = "Finalizada"
CATEGORIA_CORRETIVA = "Ação Corretiva"
CATEGORIA_CORRECAO = "Correção"
CATEGORIA_OUTROS = "Outros"

View File

@ -179,17 +179,6 @@ def parse_ocorrencias(caminho: str) -> tuple[list[OcorrenciaExtraida], list[str]
# preenchida, é o caso "Análise sem Ação" (regra #8 da skill).
if _texto(campo(linha, "Descrição da Análise da Ocorrência")):
ocorrencia.tem_linha_analise_sem_acao = True
# Sem ação, "Data de Finalização"/"Fase" descrevem o
# fechamento da própria ocorrência (não de uma ação) — ex.:
# "Elogios de Clientes", que o Sigsistem finaliza direto sem
# exigir análise/ação nenhuma (bug real: antes disso, essas
# ocorrências ficavam "Sem Análise" pra sempre no Portal).
data_fin = _parse_data(campo(linha, "Data de Finalização"))
if data_fin:
ocorrencia.data_finalizacao = data_fin
fase_linha = _texto(campo(linha, "Fase"))
if fase_linha:
ocorrencia.fase = fase_linha
continue
ocorrencia.acoes.append(

View File

@ -76,11 +76,6 @@ class OcorrenciaExtraida:
# original) — checado por linha durante o parsing, não pelo agregado
# `descricao_analise` (que só reflete a primeira linha da ocorrência).
tem_linha_analise_sem_acao: bool = False
# Vêm de uma linha SEM ação onde o Sigsistem fecha a própria ocorrência
# direto (ex.: "Elogios de Clientes") — ver NCOcorrencia.data_finalizacao/
# fase em models.py pro porquê de reaproveitar essas colunas.
data_finalizacao: date | None = None
fase: str = ""
@property
def sem_analise(self) -> bool:

View File

@ -2650,8 +2650,6 @@ def _campos_nc_ocorrencia(ocorrencia_extraida: Any) -> dict[str, Any]:
"responsavel_analise": ocorrencia_extraida.responsavel_analise,
"data_analise": ocorrencia_extraida.data_analise,
"analise_sem_acao_detectada": ocorrencia_extraida.tem_linha_analise_sem_acao,
"data_finalizacao": ocorrencia_extraida.data_finalizacao,
"fase": ocorrencia_extraida.fase,
}
@ -2974,34 +2972,9 @@ class NCOcorrenciaViewSet(viewsets.ModelViewSet):
if params.get("status_tratativa"):
queryset = queryset.filter(status_tratativa=params["status_tratativa"])
if params.get("sem_analise") == "true":
# Exclui ocorrência que o Sigsistem já fechou sem exigir análise
# (ex.: "Elogios de Clientes") — bug real: sem isso, essas
# ficavam "Sem Análise" pra sempre, exigindo "Marcar como
# tratado" manual pra algo que já está resolvido no Sigsistem.
# Duas formas de "já fechada" (a mesma ocorrência real já
# apareceu nas duas): (a) não tem nenhuma ação — nesse caso quem
# carrega "Fase" é a própria ocorrência (ver
# nao_conformidades/leiaute_ocorrencias.py); (b) tem ação(ões), e
# TODAS elas já têm `data_finalizacao` preenchida — nesse caso é
# a ação que carrega a finalização, não a ocorrência.
queryset = (
queryset.filter(descricao_analise="")
.annotate(
total_acoes=Count("acoes", distinct=True),
acoes_abertas=Count("acoes", filter=Q(acoes__data_finalizacao__isnull=True), distinct=True),
)
.exclude(Q(fase=nc_classificacao.FASE_FINALIZADA) | Q(total_acoes__gt=0, acoes_abertas=0))
)
queryset = queryset.filter(descricao_analise="")
if params.get("analise_sem_acao") == "true":
# Exclui ocorrência já finalizada no Sigsistem sem nunca ter tido
# uma ação (decisão explícita do usuário, mesmo sabendo que boa
# parte são NC/Reclamação de Cliente reais sem Ação Corretiva
# formal — confia que o indicador "NCs sem ação corretiva" do
# dashboard, que não filtra por finalizada, continua cobrindo
# esses casos pra quem olha o Dashboard).
queryset = queryset.filter(analise_sem_acao_detectada=True).exclude(
fase=nc_classificacao.FASE_FINALIZADA
)
queryset = queryset.filter(analise_sem_acao_detectada=True)
if params.get("search"):
termo = params["search"]
queryset = queryset.filter(Q(assunto__icontains=termo) | Q(codigo__icontains=termo))
@ -3032,13 +3005,10 @@ class NCOcorrenciaViewSet(viewsets.ModelViewSet):
return Response(NCOcorrenciaSerializer(ocorrencia).data)
def _status_prazo_q(bucket: str, hoje: date) -> Q:
"""Devolve a condição de UM bucket como `Q`, pra poder combinar vários
buckets com OR (`_filtra_status_prazo` abaixo) — o frontend permite
marcar mais de um ao mesmo tempo (ex.: "Vencidas" + "Vence em 7 dias").
Replica os mesmos cortes de `nao_conformidades.classificacao.status_prazo()`
como filtros de data sobre `vencimento_efetivo` (o cálculo em si roda em
Python, não dá pra usar direto num `.filter()`).
def _filtra_status_prazo(queryset: QuerySet[NCAcao], bucket: str, hoje: date) -> QuerySet[NCAcao]:
"""Replica os mesmos cortes de `nao_conformidades.classificacao.status_prazo()`
como filtros de data sobre `vencimento_efetivo`, pra poder filtrar no banco
(o cálculo em si roda em Python, não dá pra usar direto num `.filter()`).
Todo bucket de prazo (vencida/vence em X dias/no prazo/sem vencimento)
exclui ações já finalizadas — bug real corrigido: um export com
@ -3047,35 +3017,26 @@ def _status_prazo_q(bucket: str, hoje: date) -> Q:
esmagadora maioria aparecer como "vencida" sem estar realmente em
aberto."""
if bucket == nc_classificacao.STATUS_FINALIZADA:
return Q(data_finalizacao__isnull=False)
return queryset.filter(data_finalizacao__isnull=False)
aberta = Q(data_finalizacao__isnull=True)
queryset = queryset.filter(data_finalizacao__isnull=True)
if bucket == nc_classificacao.STATUS_NAO_FINALIZADA:
return aberta
return queryset
if bucket == nc_classificacao.STATUS_VENCIDA:
return aberta & Q(vencimento_efetivo__lt=hoje)
return queryset.filter(vencimento_efetivo__lt=hoje)
if bucket == nc_classificacao.STATUS_VENCE_7:
return aberta & Q(vencimento_efetivo__gte=hoje, vencimento_efetivo__lte=hoje + timedelta(days=7))
return queryset.filter(vencimento_efetivo__gte=hoje, vencimento_efetivo__lte=hoje + timedelta(days=7))
if bucket == nc_classificacao.STATUS_VENCE_30:
return aberta & Q(
return queryset.filter(
vencimento_efetivo__gt=hoje + timedelta(days=7), vencimento_efetivo__lte=hoje + timedelta(days=30)
)
if bucket == nc_classificacao.STATUS_NO_PRAZO:
return aberta & Q(vencimento_efetivo__gt=hoje + timedelta(days=30))
return queryset.filter(vencimento_efetivo__gt=hoje + timedelta(days=30))
if bucket == nc_classificacao.STATUS_SEM_VENCIMENTO:
return aberta & Q(vencimento_efetivo__isnull=True)
return queryset.filter(vencimento_efetivo__isnull=True)
raise ValidationError({"status_prazo": "Valor inválido."})
def _filtra_status_prazo(queryset: QuerySet[NCAcao], buckets: list[str], hoje: date) -> QuerySet[NCAcao]:
"""Combina um ou mais buckets de prazo com OR (filtro múltiplo do
frontend — ver `?status_prazo=` repetido em `NCAcaoViewSet.get_queryset`)."""
combinado = Q()
for bucket in buckets:
combinado |= _status_prazo_q(bucket, hoje)
return queryset.filter(combinado)
class NCAcaoViewSet(viewsets.ModelViewSet):
"""Ações persistidas — só leitura + tratativa (mesmo espírito de
NCOcorrenciaViewSet)."""
@ -3096,9 +3057,9 @@ class NCAcaoViewSet(viewsets.ModelViewSet):
params = self.request.query_params
if params.get("status_tratativa"):
queryset = queryset.filter(status_tratativa=params["status_tratativa"])
status_prazo_list = params.getlist("status_prazo")
if status_prazo_list:
queryset = _filtra_status_prazo(queryset, status_prazo_list, timezone.localdate())
status_prazo = params.get("status_prazo")
if status_prazo:
queryset = _filtra_status_prazo(queryset, status_prazo, timezone.localdate())
if params.get("search"):
termo = params["search"]
queryset = queryset.filter(

View File

@ -256,73 +256,6 @@
color: var(--text-muted);
}
/* Resumo copiável (canto superior direito da Gestão) — mesmo mecanismo de
popover position:fixed do filtro de coluna acima (ver
pidNcfFecharPopoversColuna() em nao-conformidades.js, que fecha os dois
tipos de popover de um jeito só). */
.ncf-resumo-wrap {
position: relative;
flex-shrink: 0;
}
.ncf-resumo-btn {
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.ncf-resumo-popover {
position: fixed;
z-index: 30;
width: 320px;
max-height: 70vh;
overflow-y: auto;
padding: var(--space-4);
border-radius: var(--radius-md);
border: 1px solid var(--border-subtle);
background: var(--bg-surface-raised);
box-shadow: var(--shadow-lg);
animation: pidFadeSlideUp 140ms ease;
}
.ncf-resumo-popover__preview {
font-size: 0.85rem;
line-height: 1.5;
}
.ncf-resumo-titulo {
font-weight: 700;
margin: var(--space-3) 0 var(--space-1);
}
.ncf-resumo-titulo:first-child {
margin-top: 0;
}
.ncf-resumo-linha {
margin: 0;
color: var(--text-primary);
}
.ncf-resumo-linha--vencida {
color: var(--danger);
}
.ncf-resumo-linha--vence7 {
color: var(--gold);
}
.ncf-resumo-vazio {
color: var(--text-muted);
margin: 0;
}
.ncf-resumo-popover__actions {
display: flex;
justify-content: flex-end;
margin-top: var(--space-3);
}
/* Dashboard */
.ncf-dashboard-toolbar {
display: flex;
@ -568,22 +501,12 @@
color: var(--text-primary);
}
/* Busca compartilhada da Gestão + Resumo copiável (canto superior direito) */
/* Busca compartilhada da Gestão */
.ncf-gestao-toolbar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
flex-wrap: wrap;
margin-bottom: var(--space-5);
}
.ncf-gestao-toolbar__busca {
display: flex;
align-items: center;
gap: var(--space-3);
flex: 1;
min-width: 260px;
margin-bottom: var(--space-5);
}
.ncf-gestao-search {

View File

@ -215,13 +215,10 @@ document.addEventListener("DOMContentLoaded", async () => {
let semAnaliseTodos = [];
let analiseSemAcaoTodos = [];
let acoesTodos = [];
// Filtro de Prazo é múltiplo (pedido explícito do usuário — poder ver
// "Vencidas" e "Vence em 7 dias" ao mesmo tempo, por exemplo): cada chip
// liga/desliga independente, combinados com OR no servidor (ver
// `_filtra_status_prazo` em views.py). Todos os buckets em aberto vêm
// marcados por padrão — só "Finalizadas" começa desmarcado.
const PID_NCF_PRAZO_BUCKETS_ABERTO = ["vencida", "vence_7_dias", "vence_30_dias", "no_prazo", "sem_vencimento"];
let prazoFiltrosSelecionados = new Set(PID_NCF_PRAZO_BUCKETS_ABERTO);
// "nao_finalizada" (não "todas") é o padrão — a tela deve abrir só com o
// que ainda está em aberto (data_finalizacao vazia no Sigsistem), pedido
// explícito do usuário; "Todas" continua disponível como chip separado.
let prazoFiltroAtual = "nao_finalizada";
let tratativaFiltroAtual = "pendente";
let semAnaliseTratativaFiltroAtual = "pendente";
let analiseSemAcaoTratativaFiltroAtual = "pendente";
@ -245,13 +242,6 @@ document.addEventListener("DOMContentLoaded", async () => {
let vencimentoFiltroDe = "";
let vencimentoFiltroAte = "";
// Espelham exatamente o que está sendo mostrado em cada tabela (depois de
// busca/chips/filtro de coluna/ordenação) — alimentam o Resumo copiável
// (ver atualizarResumoGestao()), atualizados no fim de cada render*().
let acoesItensVisiveis = [];
let semAnaliseItensVisiveis = [];
let analiseSemAcaoItensVisiveis = [];
const gestaoSearch = document.getElementById("ncf-gestao-search");
const gestaoSearchClearBtn = document.getElementById("ncf-gestao-search-clear-btn");
const filtroPrazoEl = document.getElementById("ncf-filtro-prazo");
@ -289,24 +279,11 @@ document.addEventListener("DOMContentLoaded", async () => {
Array.from(container.children).forEach((btn) => btn.classList.toggle("is-active", btn.dataset[datasetAttr] === valor));
}
// Mesma ideia de atualizarChipsAtivos(), mas pra um filtro de múltipla
// escolha (Prazo de Ações) — ativo se o valor do chip estiver no Set.
function atualizarChipsMultiAtivos(container, datasetAttr, selecionados) {
Array.from(container.children).forEach((btn) =>
btn.classList.toggle("is-active", selecionados.has(btn.dataset[datasetAttr]))
);
}
filtroPrazoEl.addEventListener("click", (evento) => {
const btn = evento.target.closest("[data-ncf-prazo]");
if (!btn) return;
const valor = btn.dataset.ncfPrazo;
if (prazoFiltrosSelecionados.has(valor)) {
prazoFiltrosSelecionados.delete(valor);
} else {
prazoFiltrosSelecionados.add(valor);
}
atualizarChipsMultiAtivos(filtroPrazoEl, "ncfPrazo", prazoFiltrosSelecionados);
prazoFiltroAtual = btn.dataset.ncfPrazo;
atualizarChipsAtivos(filtroPrazoEl, "ncfPrazo", prazoFiltroAtual);
carregarAcoes();
});
filtroTratativaEl.addEventListener("click", (evento) => {
@ -376,8 +353,6 @@ document.addEventListener("DOMContentLoaded", async () => {
]);
itens = pidNcfAplicarSort(itens, semAnaliseSortKey, semAnaliseSortDir, PID_NCF_SEM_ANALISE_SORT_VALOR);
pidNcfAtualizarSortHeaders(semAnaliseSortHeaders, semAnaliseSortKey, semAnaliseSortDir);
semAnaliseItensVisiveis = itens;
atualizarResumoGestao();
pidNcfAtualizarContador(semAnaliseCount, itens.length);
semAnaliseBody.innerHTML = "";
semAnaliseEmpty.hidden = itens.length > 0;
@ -454,8 +429,6 @@ document.addEventListener("DOMContentLoaded", async () => {
]);
itens = pidNcfAplicarSort(itens, analiseSemAcaoSortKey, analiseSemAcaoSortDir, PID_NCF_ANALISE_SEM_ACAO_SORT_VALOR);
pidNcfAtualizarSortHeaders(analiseSemAcaoSortHeaders, analiseSemAcaoSortKey, analiseSemAcaoSortDir);
analiseSemAcaoItensVisiveis = itens;
atualizarResumoGestao();
pidNcfAtualizarContador(analiseSemAcaoCount, itens.length);
analiseSemAcaoBody.innerHTML = "";
analiseSemAcaoEmpty.hidden = itens.length > 0;
@ -573,18 +546,8 @@ document.addEventListener("DOMContentLoaded", async () => {
);
async function carregarAcoes() {
if (prazoFiltrosSelecionados.size === 0) {
// Nenhum chip de Prazo marcado — mesmo espírito de um checklist todo
// desmarcado, não mostra nada (em vez de interpretar como "sem
// filtro" e trazer tudo, o que seria surpreendente pro usuário).
acoesTodos = [];
tipoOcorrenciaOpcoesDisponiveis = [];
renderTipoOcorrenciaChecklist();
renderAcoes();
return;
}
const params = new URLSearchParams();
prazoFiltrosSelecionados.forEach((bucket) => params.append("status_prazo", bucket));
if (prazoFiltroAtual !== "todas") params.set("status_prazo", prazoFiltroAtual);
if (tratativaFiltroAtual !== "todas") params.set("status_tratativa", tratativaFiltroAtual);
const query = params.toString();
acoesTodos = await pidApiRequest(`/nao-conformidades/acoes/${query ? "?" + query : ""}`);
@ -619,8 +582,6 @@ document.addEventListener("DOMContentLoaded", async () => {
itens = pidNcfFiltraColunaAcoes(itens);
itens = pidNcfAplicarSort(itens, acoesSortKey, acoesSortDir, PID_NCF_ACOES_SORT_VALOR);
pidNcfAtualizarSortHeaders(acoesSortHeaders, acoesSortKey, acoesSortDir);
acoesItensVisiveis = itens;
atualizarResumoGestao();
pidNcfAtualizarContador(acoesCount, itens.length);
acoesBody.innerHTML = "";
acoesEmpty.hidden = itens.length > 0;
@ -787,13 +748,6 @@ document.addEventListener("DOMContentLoaded", async () => {
function pidNcfFecharPopoversColuna() {
acoesTableEl.querySelectorAll(".ncf-col-filtro-popover").forEach((p) => (p.hidden = true));
acoesTableEl.querySelectorAll("[data-ncf-col-filtro]").forEach((b) => b.setAttribute("aria-expanded", "false"));
// Também fecha o popover do Resumo (ver seção própria mais abaixo) —
// reaproveitado aqui pra não duplicar a lógica de "só um popover aberto
// por vez" em dois lugares.
if (resumoPopover && !resumoPopover.hidden) {
resumoPopover.hidden = true;
resumoBtn.setAttribute("aria-expanded", "false");
}
}
acoesTableEl.addEventListener("click", (evento) => {
@ -853,7 +807,7 @@ document.addEventListener("DOMContentLoaded", async () => {
});
document.addEventListener("click", (evento) => {
if (evento.target.closest(".ncf-th-filterable") || evento.target.closest(".ncf-resumo-wrap")) return;
if (evento.target.closest(".ncf-th-filterable")) return;
pidNcfFecharPopoversColuna();
});
document.addEventListener("keydown", (evento) => {
@ -865,96 +819,6 @@ document.addEventListener("DOMContentLoaded", async () => {
window.addEventListener("scroll", () => pidNcfFecharPopoversColuna(), true);
window.addEventListener("resize", () => pidNcfFecharPopoversColuna());
// ---------------- Resumo copiável (Ações em aberto / Sem análise / Análise sem ação) ----------------
// Reflete exatamente o que está visível em cada uma das 3 tabelas da
// Gestão no momento (depois de busca/chips/filtro de coluna/ordenação) —
// ver os `*ItensVisiveis` atualizados no fim de cada render*() acima.
const resumoBtn = document.getElementById("ncf-resumo-btn");
const resumoPopover = document.getElementById("ncf-resumo-popover");
const resumoPreview = document.getElementById("ncf-resumo-preview");
const resumoCount = document.getElementById("ncf-resumo-count");
const resumoCopiarBtn = document.getElementById("ncf-resumo-copiar-btn");
let resumoTextoAtual = "";
function pidNcfDescricaoLinhaAcao(acaoItem) {
const data = pidNcfFormatDate(acaoItem.vencimento_efetivo);
const codigo = `${acaoItem.ocorrencia_codigo}/${acaoItem.codigo}`;
if (acaoItem.status_prazo === "vencida") return { texto: `${codigo} - Vencida em ${data}`, cor: "vencida" };
if (acaoItem.status_prazo === "vence_7_dias") return { texto: `${codigo} - Vencerá em ${data}`, cor: "vence7" };
if (acaoItem.status_prazo === "sem_vencimento") return { texto: `${codigo} - Sem vencimento definido`, cor: "normal" };
if (acaoItem.status_prazo === "finalizada") return { texto: `${codigo} - Finalizada em ${data}`, cor: "normal" };
return { texto: `${codigo} - Vencerá em ${data}`, cor: "normal" }; // vence_30_dias / no_prazo
}
function pidNcfDescricaoLinhaOcorrencia(oc) {
return { texto: `nº ${oc.codigo} - emitida em ${pidNcfFormatDate(oc.data_emissao)}`, cor: "normal" };
}
function montarBlocosResumo() {
const blocos = [];
if (acoesItensVisiveis.length) {
blocos.push({ titulo: "Ações em aberto:", linhas: acoesItensVisiveis.map(pidNcfDescricaoLinhaAcao) });
}
if (semAnaliseItensVisiveis.length) {
blocos.push({ titulo: "Ocorrência sem análise:", linhas: semAnaliseItensVisiveis.map(pidNcfDescricaoLinhaOcorrencia) });
}
if (analiseSemAcaoItensVisiveis.length) {
blocos.push({ titulo: "Análise sem ação:", linhas: analiseSemAcaoItensVisiveis.map(pidNcfDescricaoLinhaOcorrencia) });
}
return blocos;
}
function pidNcfResumoTextoPlano(blocos) {
return blocos.map((bloco) => `${bloco.titulo}\n${bloco.linhas.map((l) => l.texto).join("\n")}`).join("\n\n");
}
function pidNcfResumoHtml(blocos) {
if (!blocos.length) return '<p class="ncf-resumo-vazio">Nada pendente com os filtros atuais.</p>';
return blocos
.map(
(bloco) => `
<p class="ncf-resumo-titulo">${pidNcfEscapeHtml(bloco.titulo)}</p>
${bloco.linhas
.map((l) => `<p class="ncf-resumo-linha ncf-resumo-linha--${l.cor}">${pidNcfEscapeHtml(l.texto)}</p>`)
.join("")}`
)
.join("");
}
function atualizarResumoGestao() {
const blocos = montarBlocosResumo();
resumoTextoAtual = pidNcfResumoTextoPlano(blocos);
const totalLinhas = blocos.reduce((soma, bloco) => soma + bloco.linhas.length, 0);
pidNcfAtualizarContador(resumoCount, totalLinhas);
resumoPreview.innerHTML = pidNcfResumoHtml(blocos);
}
resumoBtn.addEventListener("click", () => {
const jaAberto = !resumoPopover.hidden;
pidNcfFecharPopoversColuna();
if (!jaAberto) {
const rect = resumoBtn.getBoundingClientRect();
const left = Math.min(rect.right - 320, window.innerWidth - 328);
resumoPopover.style.top = `${rect.bottom + 6}px`;
resumoPopover.style.left = `${Math.max(left, 8)}px`;
}
resumoPopover.hidden = jaAberto;
resumoBtn.setAttribute("aria-expanded", String(!jaAberto));
});
resumoCopiarBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(resumoTextoAtual);
const textoOriginal = resumoCopiarBtn.textContent;
resumoCopiarBtn.textContent = "Copiado!";
setTimeout(() => {
resumoCopiarBtn.textContent = textoOriginal;
}, 1500);
} catch (e) {
await pidAlert("Não foi possível copiar o texto.");
}
});
// ---------------- Drill-down do Dashboard pra Gestão ----------------
async function irParaGestaoComBusca(valor) {
ativarTab("gestao");
@ -973,7 +837,7 @@ document.addEventListener("DOMContentLoaded", async () => {
// Define o filtro ANTES de carregar — se a Gestão ainda não tinha sido
// aberta, o primeiro carregamento já sai com o filtro certo; se já
// tinha, recarrega só as Ações (sem repetir Sem Análise/Análise sem Ação).
prazoFiltrosSelecionados = new Set([bucket]);
prazoFiltroAtual = bucket;
tratativaFiltroAtual = "todas";
// Zera os filtros de coluna (Tipo de Ocorrência/Vencimento) — é uma
// navegação "leve a tela pra este estado", não deveria carregar filtro
@ -985,7 +849,7 @@ document.addEventListener("DOMContentLoaded", async () => {
const vencimentoAteInput = document.getElementById("ncf-col-filtro-vencimento-ate");
if (vencimentoDeInput) vencimentoDeInput.value = "";
if (vencimentoAteInput) vencimentoAteInput.value = "";
atualizarChipsMultiAtivos(filtroPrazoEl, "ncfPrazo", prazoFiltrosSelecionados);
atualizarChipsAtivos(filtroPrazoEl, "ncfPrazo", bucket);
atualizarChipsAtivos(filtroTratativaEl, "ncfTratativa", "todas");
ativarTab("gestao");
if (gestaoCarregada) {

View File

@ -399,101 +399,9 @@
<div class="pa-tab-panel" data-ncf-panel="gestao" hidden>
<div class="ncf-gestao-toolbar">
<div class="ncf-gestao-toolbar__busca">
<input type="search" id="ncf-gestao-search" class="ncf-gestao-search" placeholder="Buscar por colaborador, cliente/empresa ou assunto..." autocomplete="off" />
<button type="button" class="btn-outline" id="ncf-gestao-search-clear-btn" hidden>Limpar</button>
</div>
<div class="ncf-resumo-wrap">
<button type="button" class="btn-outline ncf-resumo-btn" id="ncf-resumo-btn" aria-expanded="false">
Resumo <span class="ncf-count" id="ncf-resumo-count"></span>
</button>
<div class="ncf-resumo-popover" id="ncf-resumo-popover" hidden>
<div class="ncf-resumo-popover__preview" id="ncf-resumo-preview"></div>
<div class="ncf-resumo-popover__actions">
<button type="button" class="btn-solid" id="ncf-resumo-copiar-btn">Copiar texto</button>
</div>
</div>
</div>
</div>
<section class="ncf-section" id="ncf-section-acoes">
<div class="ncf-section__header">
<h3>Ações</h3>
<span class="ncf-count" id="ncf-acoes-count"></span>
</div>
<div class="ncf-filtro-grupo">
<span class="ncf-filtro-grupo__label">Prazo:</span>
<div class="ncf-chip-row" id="ncf-filtro-prazo">
<button type="button" class="ncf-chip is-active" data-ncf-prazo="vencida">Vencidas</button>
<button type="button" class="ncf-chip is-active" data-ncf-prazo="vence_7_dias">Vence em 7 dias</button>
<button type="button" class="ncf-chip is-active" data-ncf-prazo="vence_30_dias">Vence em 30 dias</button>
<button type="button" class="ncf-chip is-active" data-ncf-prazo="no_prazo">No prazo</button>
<button type="button" class="ncf-chip is-active" data-ncf-prazo="sem_vencimento">Sem vencimento</button>
<button type="button" class="ncf-chip" data-ncf-prazo="finalizada">Finalizadas</button>
</div>
</div>
<div class="ncf-filtro-grupo">
<span class="ncf-filtro-grupo__label">Tratativa:</span>
<div class="ncf-chip-row" id="ncf-filtro-tratativa">
<button type="button" class="ncf-chip" data-ncf-tratativa="todas">Todas</button>
<button type="button" class="ncf-chip is-active" data-ncf-tratativa="pendente">Pendentes</button>
<button type="button" class="ncf-chip" data-ncf-tratativa="tratado">Tratadas</button>
</div>
</div>
<div class="pa-table-wrap">
<table class="pa-table ncf-table-sortable" id="ncf-table-acoes">
<thead>
<tr>
<th></th>
<th data-sort="ocorrencia_codigo">Ocorrência/Ação <span class="ncf-sort-icon">↕</span></th>
<th data-sort="ocorrencia_area">Área <span class="ncf-sort-icon">↕</span></th>
<th data-sort="tipo_acao">Tipo <span class="ncf-sort-icon">↕</span></th>
<th class="ncf-th-filterable" data-sort="ocorrencia_tipo_ocorrencia">
<span class="ncf-th-sort-label">Tipo de Ocorrência <span class="ncf-sort-icon">↕</span></span>
<button type="button" class="ncf-col-filtro-btn" data-ncf-col-filtro="tipo_ocorrencia" aria-label="Filtrar por tipo de ocorrência" aria-expanded="false">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 5h16l-6 8v5l-4 2v-7z" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ncf-col-filtro-popover" data-ncf-col-filtro-popover="tipo_ocorrencia" hidden>
<div class="checklist-box" id="ncf-col-filtro-tipo-lista"></div>
<p class="checklist-empty" id="ncf-col-filtro-tipo-empty" hidden>Nenhum tipo carregado.</p>
<div class="ncf-col-filtro-popover__actions">
<button type="button" class="btn-outline" data-ncf-col-filtro-limpar="tipo_ocorrencia">Limpar</button>
<button type="button" class="btn-solid" data-ncf-col-filtro-aplicar="tipo_ocorrencia">Aplicar</button>
</div>
</div>
</th>
<th data-sort="executor">Responsável <span class="ncf-sort-icon">↕</span></th>
<th class="ncf-th-filterable" data-sort="vencimento_efetivo">
<span class="ncf-th-sort-label">Vencimento <span class="ncf-sort-icon">↕</span></span>
<button type="button" class="ncf-col-filtro-btn" data-ncf-col-filtro="vencimento" aria-label="Filtrar por período de vencimento" aria-expanded="false">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 5h16l-6 8v5l-4 2v-7z" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ncf-col-filtro-popover" data-ncf-col-filtro-popover="vencimento" hidden>
<div class="modal-field">
<label for="ncf-col-filtro-vencimento-de">De</label>
<input type="date" id="ncf-col-filtro-vencimento-de" />
</div>
<div class="modal-field">
<label for="ncf-col-filtro-vencimento-ate">Até</label>
<input type="date" id="ncf-col-filtro-vencimento-ate" />
</div>
<div class="ncf-col-filtro-popover__actions">
<button type="button" class="btn-outline" data-ncf-col-filtro-limpar="vencimento">Limpar</button>
<button type="button" class="btn-solid" data-ncf-col-filtro-aplicar="vencimento">Aplicar</button>
</div>
</div>
</th>
<th data-sort="ultimo_acompanhamento_em">Último Acompanhamento <span class="ncf-sort-icon">↕</span></th>
<th data-sort="status_prazo">Prazo <span class="ncf-sort-icon">↕</span></th>
<th data-sort="status_tratativa">Tratativa <span class="ncf-sort-icon">↕</span></th>
<th></th>
</tr>
</thead>
<tbody id="ncf-acoes-body"></tbody>
</table>
</div>
<p class="ncf-empty" id="ncf-acoes-empty" hidden>Nenhuma ação encontrada para este filtro.</p>
</section>
<section class="ncf-section" id="ncf-section-sem-analise">
<div class="ncf-section__header">
@ -562,6 +470,87 @@
</div>
<p class="ncf-empty" id="ncf-analise-sem-acao-empty" hidden>Nenhuma análise sem ação no momento.</p>
</section>
<section class="ncf-section" id="ncf-section-acoes">
<div class="ncf-section__header">
<h3>Ações</h3>
<span class="ncf-count" id="ncf-acoes-count"></span>
</div>
<div class="ncf-filtro-grupo">
<span class="ncf-filtro-grupo__label">Prazo:</span>
<div class="ncf-chip-row" id="ncf-filtro-prazo">
<button type="button" class="ncf-chip" data-ncf-prazo="todas">Todas</button>
<button type="button" class="ncf-chip is-active" data-ncf-prazo="nao_finalizada">Em aberto</button>
<button type="button" class="ncf-chip" data-ncf-prazo="vencida">Vencidas</button>
<button type="button" class="ncf-chip" data-ncf-prazo="vence_7_dias">Vence em 7 dias</button>
<button type="button" class="ncf-chip" data-ncf-prazo="vence_30_dias">Vence em 30 dias</button>
<button type="button" class="ncf-chip" data-ncf-prazo="no_prazo">No prazo</button>
<button type="button" class="ncf-chip" data-ncf-prazo="sem_vencimento">Sem vencimento</button>
<button type="button" class="ncf-chip" data-ncf-prazo="finalizada">Finalizadas</button>
</div>
</div>
<div class="ncf-filtro-grupo">
<span class="ncf-filtro-grupo__label">Tratativa:</span>
<div class="ncf-chip-row" id="ncf-filtro-tratativa">
<button type="button" class="ncf-chip" data-ncf-tratativa="todas">Todas</button>
<button type="button" class="ncf-chip is-active" data-ncf-tratativa="pendente">Pendentes</button>
<button type="button" class="ncf-chip" data-ncf-tratativa="tratado">Tratadas</button>
</div>
</div>
<div class="pa-table-wrap">
<table class="pa-table ncf-table-sortable" id="ncf-table-acoes">
<thead>
<tr>
<th></th>
<th data-sort="ocorrencia_codigo">Ocorrência/Ação <span class="ncf-sort-icon">↕</span></th>
<th data-sort="ocorrencia_area">Área <span class="ncf-sort-icon">↕</span></th>
<th data-sort="tipo_acao">Tipo <span class="ncf-sort-icon">↕</span></th>
<th class="ncf-th-filterable" data-sort="ocorrencia_tipo_ocorrencia">
<span class="ncf-th-sort-label">Tipo de Ocorrência <span class="ncf-sort-icon">↕</span></span>
<button type="button" class="ncf-col-filtro-btn" data-ncf-col-filtro="tipo_ocorrencia" aria-label="Filtrar por tipo de ocorrência" aria-expanded="false">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 5h16l-6 8v5l-4 2v-7z" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ncf-col-filtro-popover" data-ncf-col-filtro-popover="tipo_ocorrencia" hidden>
<div class="checklist-box" id="ncf-col-filtro-tipo-lista"></div>
<p class="checklist-empty" id="ncf-col-filtro-tipo-empty" hidden>Nenhum tipo carregado.</p>
<div class="ncf-col-filtro-popover__actions">
<button type="button" class="btn-outline" data-ncf-col-filtro-limpar="tipo_ocorrencia">Limpar</button>
<button type="button" class="btn-solid" data-ncf-col-filtro-aplicar="tipo_ocorrencia">Aplicar</button>
</div>
</div>
</th>
<th data-sort="executor">Responsável <span class="ncf-sort-icon">↕</span></th>
<th class="ncf-th-filterable" data-sort="vencimento_efetivo">
<span class="ncf-th-sort-label">Vencimento <span class="ncf-sort-icon">↕</span></span>
<button type="button" class="ncf-col-filtro-btn" data-ncf-col-filtro="vencimento" aria-label="Filtrar por período de vencimento" aria-expanded="false">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 5h16l-6 8v5l-4 2v-7z" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ncf-col-filtro-popover" data-ncf-col-filtro-popover="vencimento" hidden>
<div class="modal-field">
<label for="ncf-col-filtro-vencimento-de">De</label>
<input type="date" id="ncf-col-filtro-vencimento-de" />
</div>
<div class="modal-field">
<label for="ncf-col-filtro-vencimento-ate">Até</label>
<input type="date" id="ncf-col-filtro-vencimento-ate" />
</div>
<div class="ncf-col-filtro-popover__actions">
<button type="button" class="btn-outline" data-ncf-col-filtro-limpar="vencimento">Limpar</button>
<button type="button" class="btn-solid" data-ncf-col-filtro-aplicar="vencimento">Aplicar</button>
</div>
</div>
</th>
<th data-sort="ultimo_acompanhamento_em">Último Acompanhamento <span class="ncf-sort-icon">↕</span></th>
<th data-sort="status_prazo">Prazo <span class="ncf-sort-icon">↕</span></th>
<th data-sort="status_tratativa">Tratativa <span class="ncf-sort-icon">↕</span></th>
<th></th>
</tr>
</thead>
<tbody id="ncf-acoes-body"></tbody>
</table>
</div>
<p class="ncf-empty" id="ncf-acoes-empty" hidden>Nenhuma ação encontrada para este filtro.</p>
</section>
</div>
<div class="pa-tab-panel" data-ncf-panel="importar" hidden>