Alterações aplicação NCs - Finalizadas
This commit is contained in:
parent
4c622f7013
commit
1f70345512
@ -17,6 +17,15 @@ STATUS_VENCE_30 = "vence_30_dias"
|
||||
STATUS_NO_PRAZO = "no_prazo"
|
||||
STATUS_SEM_VENCIMENTO = "sem_vencimento"
|
||||
|
||||
# Não é uma classificação por-ação real (status_prazo() nunca retorna isto) —
|
||||
# é só um nome de bucket agregado usado por `_filtra_status_prazo()`
|
||||
# (portal_api/views.py) e pelo chip "Em aberto" do frontend, equivalente a
|
||||
# "qualquer bucket exceto finalizada". NUNCA adicionar em STATUS_PRAZO_LABELS:
|
||||
# esse dict é iterado pra montar os contadores do dashboard, uma contagem por
|
||||
# classificação mutuamente exclusiva de ação — "não finalizada" não é
|
||||
# mutuamente exclusiva com vencida/vence_7_dias/etc, é a união delas.
|
||||
STATUS_NAO_FINALIZADA = "nao_finalizada"
|
||||
|
||||
STATUS_PRAZO_LABELS = {
|
||||
STATUS_FINALIZADA: "Finalizada",
|
||||
STATUS_VENCIDA: "Vencida",
|
||||
|
||||
@ -2081,6 +2081,7 @@ class NCAcaoSerializer(serializers.ModelSerializer):
|
||||
# da Gestão e drill-down dos rankings do Dashboard) sem duplicar o dado.
|
||||
ocorrencia_clientes = serializers.CharField(source="ocorrencia.clientes_relacionados", read_only=True)
|
||||
ocorrencia_area = serializers.CharField(source="ocorrencia.area", read_only=True)
|
||||
ocorrencia_tipo_ocorrencia = serializers.CharField(source="ocorrencia.tipo_ocorrencia", read_only=True)
|
||||
status_prazo = serializers.SerializerMethodField()
|
||||
status_prazo_label = serializers.SerializerMethodField()
|
||||
dias_para_vencer = serializers.SerializerMethodField()
|
||||
@ -2094,6 +2095,7 @@ class NCAcaoSerializer(serializers.ModelSerializer):
|
||||
"ocorrencia_assunto",
|
||||
"ocorrencia_clientes",
|
||||
"ocorrencia_area",
|
||||
"ocorrencia_tipo_ocorrencia",
|
||||
"codigo",
|
||||
"data_emissao",
|
||||
"tipo_acao",
|
||||
|
||||
@ -3005,6 +3005,8 @@ def _filtra_status_prazo(queryset: QuerySet[NCAcao], bucket: str, hoje: date) ->
|
||||
return queryset.filter(data_finalizacao__isnull=False)
|
||||
|
||||
queryset = queryset.filter(data_finalizacao__isnull=True)
|
||||
if bucket == nc_classificacao.STATUS_NAO_FINALIZADA:
|
||||
return queryset
|
||||
if bucket == nc_classificacao.STATUS_VENCIDA:
|
||||
return queryset.filter(vencimento_efetivo__lt=hoje)
|
||||
if bucket == nc_classificacao.STATUS_VENCE_7:
|
||||
|
||||
@ -144,6 +144,118 @@
|
||||
color: var(--accent-contrast);
|
||||
}
|
||||
|
||||
/* Ordenação por clique no cabeçalho — mesma linguagem visual de #ua-table
|
||||
(perfis-acesso.css, th[data-sort]/.ua-sort-icon), duplicado aqui porque
|
||||
essa folha não compartilha código com a de usuários. Escopado pela classe
|
||||
compartilhada .ncf-table-sortable (as 3 tabelas da Gestão), não por ID
|
||||
individual. */
|
||||
.ncf-table-sortable th[data-sort] {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ncf-sort-icon {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ncf-table-sortable th[data-sort].is-sort-asc .ncf-sort-icon,
|
||||
.ncf-table-sortable th[data-sort].is-sort-desc .ncf-sort-icon {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Filtro por coluna (só "Tipo de Ocorrência"/"Vencimento", tabela de Ações) —
|
||||
mesmo espírito de .ind-info-wrap/.ind-info-popover (indicador-desempenho.css),
|
||||
mas com uma diferença importante: aqui o popover é position:fixed (não
|
||||
absolute), com top/left calculados em JS a partir do
|
||||
getBoundingClientRect() do botão (ver pidNcfAbrirPopoverColuna() em
|
||||
nao-conformidades.js). position:fixed escapa do overflow:hidden de
|
||||
.pa-table-wrap (perfis-acesso.css, usado pra arredondar o canto da
|
||||
tabela) sem precisar reabrir esse overflow — uma primeira versão fazia
|
||||
isso com overflow:visible no wrapper, mas isso quebrava a contenção da
|
||||
tabela (12 colunas já é bem larga) e estourava a largura da página. */
|
||||
.ncf-th-filterable {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ncf-th-sort-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ncf-col-filtro-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: var(--space-2);
|
||||
padding: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast), border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.ncf-col-filtro-btn:hover,
|
||||
.ncf-col-filtro-btn[aria-expanded="true"] {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.ncf-col-filtro-popover {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
width: 240px;
|
||||
padding: var(--space-3);
|
||||
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;
|
||||
/* .pa-table th força uppercase/letter-spacing/tamanho pequeno no
|
||||
cabeçalho — resetado aqui pra virar texto normal de novo. */
|
||||
font-size: 0.82rem;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary);
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ncf-col-filtro-popover .checklist-box {
|
||||
max-height: 160px;
|
||||
}
|
||||
|
||||
.ncf-col-filtro-popover .modal-field {
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.ncf-col-filtro-popover__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.ncf-col-filtro-popover__actions .btn-outline,
|
||||
.ncf-col-filtro-popover__actions .btn-solid {
|
||||
padding: 4px 12px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
/* Ação já finalizada no Sigsistem — não há nada pra Qualidade tratar, então
|
||||
a coluna de ação mostra só um traço em vez do botão "Marcar como tratado". */
|
||||
.ncf-sem-acao-necessaria {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Dashboard */
|
||||
.ncf-dashboard-toolbar {
|
||||
display: flex;
|
||||
@ -450,6 +562,32 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* A linha principal de uma Ação não fecha com borda embaixo — quem fecha é
|
||||
a linha de descrição logo abaixo (ver .ncf-acao-descricao-row), pra as
|
||||
duas lerem como um bloco só, não duas linhas de tabela separadas. */
|
||||
.pa-table tr.ncf-acao-main-row td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Linha full-width com a descrição da Ação, logo abaixo da linha principal
|
||||
(não é mais uma coluna própria — não cabia na largura da tabela). Classe
|
||||
própria com maior especificidade que .pa-table td:last-child
|
||||
(text-align:right) pelo mesmo motivo de .ncf-detail-cell acima: como usa
|
||||
colspan, é sempre "a última" célula da linha. */
|
||||
.pa-table tr.ncf-acao-descricao-row td.ncf-acao-descricao-cell {
|
||||
padding-top: 0;
|
||||
padding-bottom: var(--space-3);
|
||||
text-align: left;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.ncf-acao-descricao-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.ncf-detail-panel {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
|
||||
@ -42,6 +42,68 @@ const PID_NCF_STATUS_PRAZO_CLASSES = {
|
||||
sem_vencimento: "status-pill status-pill--neutral",
|
||||
};
|
||||
|
||||
// Compara duas strings tratando conteúdo numérico como número (ex.: "92" <
|
||||
// "150"), senão cai pra comparação alfabética pt-BR — mesmo critério de
|
||||
// comparaValoresUsuario() em users-admin.js/comparaValoresLista() em
|
||||
// importacao-plano-saude.js, copiado localmente porque cada tela do Portal
|
||||
// tem seu próprio arquivo JS autocontido, sem módulo compartilhado.
|
||||
function pidNcfCompara(a, b) {
|
||||
const an = Number(a);
|
||||
const bn = Number(b);
|
||||
const ambosNumeros = a !== "" && b !== "" && a != null && b != null && !Number.isNaN(an) && !Number.isNaN(bn);
|
||||
if (ambosNumeros) return an - bn;
|
||||
return String(a ?? "").localeCompare(String(b ?? ""), "pt-BR", { sensitivity: "base" });
|
||||
}
|
||||
|
||||
// Ordena por severidade, não alfabeticamente (ex.: "Vencida" precisa vir
|
||||
// antes de "No prazo", não é isso que localeCompare faria).
|
||||
const PID_NCF_STATUS_PRAZO_RANK = {
|
||||
vencida: 0,
|
||||
vence_7_dias: 1,
|
||||
vence_30_dias: 2,
|
||||
no_prazo: 3,
|
||||
sem_vencimento: 4,
|
||||
finalizada: 5,
|
||||
};
|
||||
const PID_NCF_STATUS_TRATATIVA_RANK = { pendente: 0, tratado: 1 };
|
||||
|
||||
function pidNcfAplicarSort(itens, sortKey, sortDir, sortValorMap) {
|
||||
if (!sortKey) return itens; // preserva a ordem que já veio do servidor até o usuário clicar um cabeçalho
|
||||
const extrator = sortValorMap[sortKey];
|
||||
if (!extrator) return itens;
|
||||
return itens.slice().sort((a, b) => pidNcfCompara(extrator(a), extrator(b)) * sortDir);
|
||||
}
|
||||
|
||||
function pidNcfAtualizarSortHeaders(headers, sortKey, sortDir) {
|
||||
headers.forEach((th) => {
|
||||
const key = th.getAttribute("data-sort");
|
||||
th.classList.toggle("is-sort-asc", key === sortKey && sortDir === 1);
|
||||
th.classList.toggle("is-sort-desc", key === sortKey && sortDir === -1);
|
||||
});
|
||||
}
|
||||
|
||||
// Liga o clique de ordenação nos <th data-sort> de uma tabela — clicar
|
||||
// alterna asc/desc na mesma coluna ou reseta pra asc numa coluna nova. Ignora
|
||||
// cliques que vieram do ícone/popover de filtro de coluna (só a tabela de
|
||||
// Ações tem isso hoje), pra sort e filtro de coluna nunca disparar juntos.
|
||||
function pidNcfBindSortHeaders(tableId, getSortState, setSortState, rerender) {
|
||||
const headers = Array.from(document.querySelectorAll(`#${tableId} th[data-sort]`));
|
||||
headers.forEach((th) => {
|
||||
th.addEventListener("click", (evento) => {
|
||||
if (evento.target.closest("[data-ncf-col-filtro], .ncf-col-filtro-popover")) return;
|
||||
const key = th.getAttribute("data-sort");
|
||||
const { sortKey, sortDir } = getSortState();
|
||||
if (sortKey === key) {
|
||||
setSortState(key, sortDir * -1);
|
||||
} else {
|
||||
setSortState(key, 1);
|
||||
}
|
||||
rerender();
|
||||
});
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** Botão de expandir (chevron que gira) + linha de detalhe associada, mesmo
|
||||
* padrão de `.cc-painel-fiscal__toggle` (custo-contratacao.js), adaptado pra
|
||||
* uma linha de tabela — `montarConteudo(painelEl)` só roda na primeira vez
|
||||
@ -153,14 +215,39 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
let semAnaliseTodos = [];
|
||||
let analiseSemAcaoTodos = [];
|
||||
let acoesTodos = [];
|
||||
let prazoFiltroAtual = "todas";
|
||||
// "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";
|
||||
let buscaGestao = "";
|
||||
|
||||
// Ordenação por clique no cabeçalho — estado independente por tabela.
|
||||
// sortKey=null preserva a ordenação vinda do servidor até o usuário clicar
|
||||
// um cabeçalho pela primeira vez.
|
||||
let semAnaliseSortKey = null;
|
||||
let semAnaliseSortDir = 1;
|
||||
let analiseSemAcaoSortKey = null;
|
||||
let analiseSemAcaoSortDir = 1;
|
||||
let acoesSortKey = null;
|
||||
let acoesSortDir = 1;
|
||||
|
||||
// Filtros de coluna da tabela de Ações (só "Tipo de Ocorrência" e
|
||||
// "Vencimento" têm isso, e só nessa tabela) — sempre client-side, sobre o
|
||||
// `acoesTodos` já carregado (ver carregarAcoes()/renderAcoes()).
|
||||
let tipoOcorrenciaFiltroSelecionados = new Set(); // vazio = sem filtro (mostra tudo)
|
||||
let tipoOcorrenciaOpcoesDisponiveis = [];
|
||||
let vencimentoFiltroDe = "";
|
||||
let vencimentoFiltroAte = "";
|
||||
|
||||
const gestaoSearch = document.getElementById("ncf-gestao-search");
|
||||
const gestaoSearchClearBtn = document.getElementById("ncf-gestao-search-clear-btn");
|
||||
const filtroPrazoEl = document.getElementById("ncf-filtro-prazo");
|
||||
const filtroTratativaEl = document.getElementById("ncf-filtro-tratativa");
|
||||
const filtroTratativaSemAnaliseEl = document.getElementById("ncf-filtro-tratativa-sem-analise");
|
||||
const filtroTratativaAnaliseSemAcaoEl = document.getElementById("ncf-filtro-tratativa-analise-sem-acao");
|
||||
|
||||
function filtrarPorBusca(itens, campos) {
|
||||
const termo = buscaGestao.trim().toLowerCase();
|
||||
@ -206,6 +293,20 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
atualizarChipsAtivos(filtroTratativaEl, "ncfTratativa", tratativaFiltroAtual);
|
||||
carregarAcoes();
|
||||
});
|
||||
filtroTratativaSemAnaliseEl.addEventListener("click", (evento) => {
|
||||
const btn = evento.target.closest("[data-ncf-tratativa]");
|
||||
if (!btn) return;
|
||||
semAnaliseTratativaFiltroAtual = btn.dataset.ncfTratativa;
|
||||
atualizarChipsAtivos(filtroTratativaSemAnaliseEl, "ncfTratativa", semAnaliseTratativaFiltroAtual);
|
||||
carregarSemAnalise();
|
||||
});
|
||||
filtroTratativaAnaliseSemAcaoEl.addEventListener("click", (evento) => {
|
||||
const btn = evento.target.closest("[data-ncf-tratativa]");
|
||||
if (!btn) return;
|
||||
analiseSemAcaoTratativaFiltroAtual = btn.dataset.ncfTratativa;
|
||||
atualizarChipsAtivos(filtroTratativaAnaliseSemAcaoEl, "ncfTratativa", analiseSemAcaoTratativaFiltroAtual);
|
||||
carregarAnaliseSemAcao();
|
||||
});
|
||||
|
||||
async function garantirGestaoCarregada() {
|
||||
if (gestaoCarregada) return;
|
||||
@ -217,20 +318,41 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const semAnaliseBody = document.getElementById("ncf-sem-analise-body");
|
||||
const semAnaliseEmpty = document.getElementById("ncf-sem-analise-empty");
|
||||
const semAnaliseCount = document.getElementById("ncf-sem-analise-count");
|
||||
const PID_NCF_SEM_ANALISE_SORT_VALOR = {
|
||||
codigo: (oc) => oc.codigo,
|
||||
data_emissao: (oc) => oc.data_emissao || "",
|
||||
area: (oc) => oc.area || "",
|
||||
emissor_relato: (oc) => oc.emissor_relato || "",
|
||||
indicado_analise: (oc) => oc.indicado_analise || "",
|
||||
status_tratativa: (oc) => PID_NCF_STATUS_TRATATIVA_RANK[oc.status_tratativa] ?? 0,
|
||||
};
|
||||
const semAnaliseSortHeaders = pidNcfBindSortHeaders(
|
||||
"ncf-table-sem-analise",
|
||||
() => ({ sortKey: semAnaliseSortKey, sortDir: semAnaliseSortDir }),
|
||||
(key, dir) => {
|
||||
semAnaliseSortKey = key;
|
||||
semAnaliseSortDir = dir;
|
||||
},
|
||||
renderSemAnalise
|
||||
);
|
||||
|
||||
async function carregarSemAnalise() {
|
||||
semAnaliseTodos = await pidApiRequest("/nao-conformidades/ocorrencias/?sem_analise=true");
|
||||
const params = new URLSearchParams({ sem_analise: "true" });
|
||||
if (semAnaliseTratativaFiltroAtual !== "todas") params.set("status_tratativa", semAnaliseTratativaFiltroAtual);
|
||||
semAnaliseTodos = await pidApiRequest(`/nao-conformidades/ocorrencias/?${params.toString()}`);
|
||||
renderSemAnalise();
|
||||
}
|
||||
|
||||
function renderSemAnalise() {
|
||||
const itens = filtrarPorBusca(semAnaliseTodos, [
|
||||
let itens = filtrarPorBusca(semAnaliseTodos, [
|
||||
"indicado_analise",
|
||||
"responsavel_analise",
|
||||
"clientes_relacionados",
|
||||
"assunto",
|
||||
"emissor_relato",
|
||||
]);
|
||||
itens = pidNcfAplicarSort(itens, semAnaliseSortKey, semAnaliseSortDir, PID_NCF_SEM_ANALISE_SORT_VALOR);
|
||||
pidNcfAtualizarSortHeaders(semAnaliseSortHeaders, semAnaliseSortKey, semAnaliseSortDir);
|
||||
pidNcfAtualizarContador(semAnaliseCount, itens.length);
|
||||
semAnaliseBody.innerHTML = "";
|
||||
semAnaliseEmpty.hidden = itens.length > 0;
|
||||
@ -272,20 +394,41 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const analiseSemAcaoBody = document.getElementById("ncf-analise-sem-acao-body");
|
||||
const analiseSemAcaoEmpty = document.getElementById("ncf-analise-sem-acao-empty");
|
||||
const analiseSemAcaoCount = document.getElementById("ncf-analise-sem-acao-count");
|
||||
const PID_NCF_ANALISE_SEM_ACAO_SORT_VALOR = {
|
||||
codigo: (oc) => oc.codigo,
|
||||
data_emissao: (oc) => oc.data_emissao || "",
|
||||
area: (oc) => oc.area || "",
|
||||
responsavel_analise: (oc) => oc.responsavel_analise || "",
|
||||
descricao_analise: (oc) => oc.descricao_analise || "",
|
||||
status_tratativa: (oc) => PID_NCF_STATUS_TRATATIVA_RANK[oc.status_tratativa] ?? 0,
|
||||
};
|
||||
const analiseSemAcaoSortHeaders = pidNcfBindSortHeaders(
|
||||
"ncf-table-analise-sem-acao",
|
||||
() => ({ sortKey: analiseSemAcaoSortKey, sortDir: analiseSemAcaoSortDir }),
|
||||
(key, dir) => {
|
||||
analiseSemAcaoSortKey = key;
|
||||
analiseSemAcaoSortDir = dir;
|
||||
},
|
||||
renderAnaliseSemAcao
|
||||
);
|
||||
|
||||
async function carregarAnaliseSemAcao() {
|
||||
analiseSemAcaoTodos = await pidApiRequest("/nao-conformidades/ocorrencias/?analise_sem_acao=true");
|
||||
const params = new URLSearchParams({ analise_sem_acao: "true" });
|
||||
if (analiseSemAcaoTratativaFiltroAtual !== "todas") params.set("status_tratativa", analiseSemAcaoTratativaFiltroAtual);
|
||||
analiseSemAcaoTodos = await pidApiRequest(`/nao-conformidades/ocorrencias/?${params.toString()}`);
|
||||
renderAnaliseSemAcao();
|
||||
}
|
||||
|
||||
function renderAnaliseSemAcao() {
|
||||
const itens = filtrarPorBusca(analiseSemAcaoTodos, [
|
||||
let itens = filtrarPorBusca(analiseSemAcaoTodos, [
|
||||
"indicado_analise",
|
||||
"responsavel_analise",
|
||||
"clientes_relacionados",
|
||||
"assunto",
|
||||
"descricao_analise",
|
||||
]);
|
||||
itens = pidNcfAplicarSort(itens, analiseSemAcaoSortKey, analiseSemAcaoSortDir, PID_NCF_ANALISE_SEM_ACAO_SORT_VALOR);
|
||||
pidNcfAtualizarSortHeaders(analiseSemAcaoSortHeaders, analiseSemAcaoSortKey, analiseSemAcaoSortDir);
|
||||
pidNcfAtualizarContador(analiseSemAcaoCount, itens.length);
|
||||
analiseSemAcaoBody.innerHTML = "";
|
||||
analiseSemAcaoEmpty.hidden = itens.length > 0;
|
||||
@ -381,6 +524,26 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const acoesBody = document.getElementById("ncf-acoes-body");
|
||||
const acoesEmpty = document.getElementById("ncf-acoes-empty");
|
||||
const acoesCount = document.getElementById("ncf-acoes-count");
|
||||
const PID_NCF_ACOES_SORT_VALOR = {
|
||||
ocorrencia_codigo: (a) => Number(a.ocorrencia_codigo) * 1000000 + Number(a.codigo || 0),
|
||||
ocorrencia_area: (a) => a.ocorrencia_area || "",
|
||||
tipo_acao: (a) => a.tipo_acao || "",
|
||||
ocorrencia_tipo_ocorrencia: (a) => a.ocorrencia_tipo_ocorrencia || "",
|
||||
executor: (a) => a.executor || "",
|
||||
vencimento_efetivo: (a) => a.vencimento_efetivo || "",
|
||||
ultimo_acompanhamento_em: (a) => a.ultimo_acompanhamento_em || "",
|
||||
status_prazo: (a) => PID_NCF_STATUS_PRAZO_RANK[a.status_prazo] ?? 99,
|
||||
status_tratativa: (a) => PID_NCF_STATUS_TRATATIVA_RANK[a.status_tratativa] ?? 0,
|
||||
};
|
||||
const acoesSortHeaders = pidNcfBindSortHeaders(
|
||||
"ncf-table-acoes",
|
||||
() => ({ sortKey: acoesSortKey, sortDir: acoesSortDir }),
|
||||
(key, dir) => {
|
||||
acoesSortKey = key;
|
||||
acoesSortDir = dir;
|
||||
},
|
||||
renderAcoes
|
||||
);
|
||||
|
||||
async function carregarAcoes() {
|
||||
const params = new URLSearchParams();
|
||||
@ -388,31 +551,65 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
if (tratativaFiltroAtual !== "todas") params.set("status_tratativa", tratativaFiltroAtual);
|
||||
const query = params.toString();
|
||||
acoesTodos = await pidApiRequest(`/nao-conformidades/acoes/${query ? "?" + query : ""}`);
|
||||
|
||||
// Opções do checklist "Tipo de Ocorrência" vêm do array cheio, ANTES de
|
||||
// qualquer filtro de coluna/busca — recalculado só aqui (uma vez por
|
||||
// carregamento do servidor), nunca dentro de renderAcoes(), pra
|
||||
// desmarcar um tipo nunca fazer os outros sumirem da lista.
|
||||
tipoOcorrenciaOpcoesDisponiveis = Array.from(
|
||||
new Set(acoesTodos.map((a) => a.ocorrencia_tipo_ocorrencia).filter(Boolean))
|
||||
).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" }));
|
||||
tipoOcorrenciaFiltroSelecionados = new Set(
|
||||
Array.from(tipoOcorrenciaFiltroSelecionados).filter((v) => tipoOcorrenciaOpcoesDisponiveis.includes(v))
|
||||
);
|
||||
renderTipoOcorrenciaChecklist();
|
||||
|
||||
renderAcoes();
|
||||
}
|
||||
|
||||
function pidNcfFiltraColunaAcoes(itens) {
|
||||
let out = itens;
|
||||
if (tipoOcorrenciaFiltroSelecionados.size > 0) {
|
||||
out = out.filter((a) => tipoOcorrenciaFiltroSelecionados.has(a.ocorrencia_tipo_ocorrencia));
|
||||
}
|
||||
if (vencimentoFiltroDe) out = out.filter((a) => a.vencimento_efetivo && a.vencimento_efetivo >= vencimentoFiltroDe);
|
||||
if (vencimentoFiltroAte) out = out.filter((a) => a.vencimento_efetivo && a.vencimento_efetivo <= vencimentoFiltroAte);
|
||||
return out;
|
||||
}
|
||||
|
||||
function renderAcoes() {
|
||||
const itens = filtrarPorBusca(acoesTodos, ["executor", "ocorrencia_clientes", "ocorrencia_codigo", "acao_texto"]);
|
||||
let itens = filtrarPorBusca(acoesTodos, ["executor", "ocorrencia_clientes", "ocorrencia_codigo", "acao_texto"]);
|
||||
itens = pidNcfFiltraColunaAcoes(itens);
|
||||
itens = pidNcfAplicarSort(itens, acoesSortKey, acoesSortDir, PID_NCF_ACOES_SORT_VALOR);
|
||||
pidNcfAtualizarSortHeaders(acoesSortHeaders, acoesSortKey, acoesSortDir);
|
||||
pidNcfAtualizarContador(acoesCount, itens.length);
|
||||
acoesBody.innerHTML = "";
|
||||
acoesEmpty.hidden = itens.length > 0;
|
||||
itens.forEach((acaoItem) => {
|
||||
const tratado = acaoItem.status_tratativa === "tratado";
|
||||
const { btn, trDetalhe } = pidNcfCriarLinhaExpandivel(10, (painel) => montarPainelAcao(painel, acaoItem));
|
||||
// 11 colunas — a descrição da Ação não é mais uma coluna própria (não
|
||||
// cabia mais na largura da tabela junto com as outras); vira uma linha
|
||||
// full-width logo abaixo, ver trDescricao mais adiante.
|
||||
const { btn, trDetalhe } = pidNcfCriarLinhaExpandivel(11, (painel) => montarPainelAcao(painel, acaoItem));
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "ncf-acao-main-row";
|
||||
const tdBtn = document.createElement("td");
|
||||
tdBtn.className = "ncf-expand-col";
|
||||
tdBtn.appendChild(btn);
|
||||
tr.appendChild(tdBtn);
|
||||
const ultimoAcompHtml = acaoItem.ultimo_acompanhamento_eh_prorrogacao
|
||||
? ` <span class="status-pill status-pill--warning">Prorrogação</span>`
|
||||
: "";
|
||||
tr.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<td>${acaoItem.ocorrencia_codigo}/${acaoItem.codigo}</td>
|
||||
<td>${pidNcfEscapeHtml(acaoItem.ocorrencia_area) || "—"}</td>
|
||||
<td>${pidNcfEscapeHtml(acaoItem.tipo_acao) || "—"}</td>
|
||||
<td>${pidNcfEscapeHtml((acaoItem.acao_texto || "").slice(0, 90))}</td>
|
||||
<td>${pidNcfEscapeHtml(acaoItem.ocorrencia_tipo_ocorrencia) || "—"}</td>
|
||||
<td>${pidNcfEscapeHtml(acaoItem.executor) || "—"}</td>
|
||||
<td>${pidNcfFormatDate(acaoItem.vencimento_efetivo)}</td>`
|
||||
<td>${pidNcfFormatDate(acaoItem.vencimento_efetivo)}</td>
|
||||
<td>${pidNcfFormatDate(acaoItem.ultimo_acompanhamento_em)}${ultimoAcompHtml}</td>`
|
||||
);
|
||||
const tdPrazo = document.createElement("td");
|
||||
const badgePrazo = document.createElement("span");
|
||||
@ -426,15 +623,37 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
tr.appendChild(tdTratativa);
|
||||
|
||||
const tdAcao = document.createElement("td");
|
||||
// Ação já finalizada no Sigsistem (data_finalizacao preenchida) — não
|
||||
// há nada pra Qualidade tratar aqui, então nem oferece o botão.
|
||||
if (acaoItem.status_prazo === "finalizada") {
|
||||
const semAcaoEl = document.createElement("span");
|
||||
semAcaoEl.className = "ncf-sem-acao-necessaria";
|
||||
semAcaoEl.textContent = "—";
|
||||
tdAcao.appendChild(semAcaoEl);
|
||||
} else {
|
||||
const tratarBtn = document.createElement("button");
|
||||
tratarBtn.type = "button";
|
||||
tratarBtn.className = tratado ? "btn-outline" : "btn-solid";
|
||||
tratarBtn.textContent = tratado ? "Reabrir" : "Marcar como tratado";
|
||||
tratarBtn.addEventListener("click", () => alternarTratativaAcao(acaoItem.id, tratado));
|
||||
tdAcao.appendChild(tratarBtn);
|
||||
}
|
||||
tr.appendChild(tdAcao);
|
||||
|
||||
// Linha full-width com a descrição da Ação — não é mais uma coluna
|
||||
// própria (não cabia na largura da tabela junto com as outras 10
|
||||
// colunas), mas o usuário precisa ver o texto sem abrir linha por
|
||||
// linha, então vira uma segunda linha logo abaixo da principal.
|
||||
const trDescricao = document.createElement("tr");
|
||||
trDescricao.className = "ncf-acao-descricao-row";
|
||||
const tdDescricao = document.createElement("td");
|
||||
tdDescricao.colSpan = 11;
|
||||
tdDescricao.className = "ncf-acao-descricao-cell";
|
||||
tdDescricao.innerHTML = `<span class="ncf-acao-descricao-label">Ação:</span> ${pidNcfEscapeHtml(acaoItem.acao_texto) || "—"}`;
|
||||
trDescricao.appendChild(tdDescricao);
|
||||
|
||||
acoesBody.appendChild(tr);
|
||||
acoesBody.appendChild(trDescricao);
|
||||
acoesBody.appendChild(trDetalhe);
|
||||
});
|
||||
}
|
||||
@ -506,6 +725,100 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Filtro de coluna (Tipo de Ocorrência / Vencimento) ----------------
|
||||
// Só existe na tabela de Ações, client-side, sobre `acoesTodos` já
|
||||
// carregado — ver pidNcfFiltraColunaAcoes()/carregarAcoes() acima.
|
||||
function renderTipoOcorrenciaChecklist() {
|
||||
const lista = document.getElementById("ncf-col-filtro-tipo-lista");
|
||||
const empty = document.getElementById("ncf-col-filtro-tipo-empty");
|
||||
empty.hidden = tipoOcorrenciaOpcoesDisponiveis.length > 0;
|
||||
lista.innerHTML = tipoOcorrenciaOpcoesDisponiveis
|
||||
.map(
|
||||
(tipo) => `
|
||||
<label class="checklist-item">
|
||||
<input type="checkbox" value="${pidNcfEscapeHtml(tipo)}" ${tipoOcorrenciaFiltroSelecionados.has(tipo) ? "checked" : ""} />
|
||||
<span>${pidNcfEscapeHtml(tipo)}</span>
|
||||
</label>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
const acoesTableEl = document.getElementById("ncf-table-acoes");
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
acoesTableEl.addEventListener("click", (evento) => {
|
||||
const filtroBtn = evento.target.closest("[data-ncf-col-filtro]");
|
||||
if (filtroBtn) {
|
||||
const popover = filtroBtn.nextElementSibling; // .ncf-col-filtro-popover
|
||||
const jaAberto = !popover.hidden;
|
||||
pidNcfFecharPopoversColuna();
|
||||
if (!jaAberto) {
|
||||
// position:fixed calculado a partir do botão — escapa do
|
||||
// overflow:hidden de .pa-table-wrap sem precisar reabri-lo (ver
|
||||
// nao-conformidades.css).
|
||||
const rect = filtroBtn.getBoundingClientRect();
|
||||
// Impede o popover (240px) de vazar pra fora da borda direita da
|
||||
// janela quando o botão está perto dela.
|
||||
const left = Math.min(rect.left, window.innerWidth - 256);
|
||||
popover.style.top = `${rect.bottom + 6}px`;
|
||||
popover.style.left = `${Math.max(left, 8)}px`;
|
||||
}
|
||||
popover.hidden = jaAberto;
|
||||
filtroBtn.setAttribute("aria-expanded", String(!jaAberto));
|
||||
return;
|
||||
}
|
||||
|
||||
const aplicarBtn = evento.target.closest("[data-ncf-col-filtro-aplicar]");
|
||||
if (aplicarBtn) {
|
||||
const alvo = aplicarBtn.getAttribute("data-ncf-col-filtro-aplicar");
|
||||
if (alvo === "tipo_ocorrencia") {
|
||||
const marcados = Array.from(document.querySelectorAll("#ncf-col-filtro-tipo-lista input:checked")).map(
|
||||
(cb) => cb.value
|
||||
);
|
||||
tipoOcorrenciaFiltroSelecionados = new Set(marcados);
|
||||
} else if (alvo === "vencimento") {
|
||||
vencimentoFiltroDe = document.getElementById("ncf-col-filtro-vencimento-de").value;
|
||||
vencimentoFiltroAte = document.getElementById("ncf-col-filtro-vencimento-ate").value;
|
||||
}
|
||||
aplicarBtn.closest(".ncf-col-filtro-popover").hidden = true;
|
||||
renderAcoes();
|
||||
return;
|
||||
}
|
||||
|
||||
const limparBtn = evento.target.closest("[data-ncf-col-filtro-limpar]");
|
||||
if (limparBtn) {
|
||||
const alvo = limparBtn.getAttribute("data-ncf-col-filtro-limpar");
|
||||
if (alvo === "tipo_ocorrencia") {
|
||||
tipoOcorrenciaFiltroSelecionados = new Set();
|
||||
renderTipoOcorrenciaChecklist();
|
||||
} else if (alvo === "vencimento") {
|
||||
vencimentoFiltroDe = "";
|
||||
vencimentoFiltroAte = "";
|
||||
document.getElementById("ncf-col-filtro-vencimento-de").value = "";
|
||||
document.getElementById("ncf-col-filtro-vencimento-ate").value = "";
|
||||
}
|
||||
limparBtn.closest(".ncf-col-filtro-popover").hidden = true;
|
||||
renderAcoes();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", (evento) => {
|
||||
if (evento.target.closest(".ncf-th-filterable")) return;
|
||||
pidNcfFecharPopoversColuna();
|
||||
});
|
||||
document.addEventListener("keydown", (evento) => {
|
||||
if (evento.key !== "Escape") return;
|
||||
pidNcfFecharPopoversColuna();
|
||||
});
|
||||
// position:fixed não acompanha o scroll sozinho — mais simples fechar o
|
||||
// popover do que recalcular a posição a cada evento de scroll/resize.
|
||||
window.addEventListener("scroll", () => pidNcfFecharPopoversColuna(), true);
|
||||
window.addEventListener("resize", () => pidNcfFecharPopoversColuna());
|
||||
|
||||
// ---------------- Drill-down do Dashboard pra Gestão ----------------
|
||||
async function irParaGestaoComBusca(valor) {
|
||||
ativarTab("gestao");
|
||||
@ -526,6 +839,16 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
// tinha, recarrega só as Ações (sem repetir Sem Análise/Análise sem Ação).
|
||||
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
|
||||
// residual de uma sessão anterior escondendo o que o card prometeu mostrar.
|
||||
tipoOcorrenciaFiltroSelecionados = new Set();
|
||||
vencimentoFiltroDe = "";
|
||||
vencimentoFiltroAte = "";
|
||||
const vencimentoDeInput = document.getElementById("ncf-col-filtro-vencimento-de");
|
||||
const vencimentoAteInput = document.getElementById("ncf-col-filtro-vencimento-ate");
|
||||
if (vencimentoDeInput) vencimentoDeInput.value = "";
|
||||
if (vencimentoAteInput) vencimentoAteInput.value = "";
|
||||
atualizarChipsAtivos(filtroPrazoEl, "ncfPrazo", bucket);
|
||||
atualizarChipsAtivos(filtroTratativaEl, "ncfTratativa", "todas");
|
||||
ativarTab("gestao");
|
||||
|
||||
@ -395,17 +395,25 @@
|
||||
<span class="ncf-count" id="ncf-sem-analise-count"></span>
|
||||
</div>
|
||||
<p class="ncf-section__hint">Ocorrências abertas sem nenhuma análise registrada — cobrar o responsável indicado.</p>
|
||||
<div class="ncf-filtro-grupo">
|
||||
<span class="ncf-filtro-grupo__label">Tratativa:</span>
|
||||
<div class="ncf-chip-row" id="ncf-filtro-tratativa-sem-analise">
|
||||
<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">
|
||||
<table class="pa-table ncf-table-sortable" id="ncf-table-sem-analise">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Código</th>
|
||||
<th>Emissão</th>
|
||||
<th>Área</th>
|
||||
<th>Emissor</th>
|
||||
<th>Indicado para análise</th>
|
||||
<th>Tratativa</th>
|
||||
<th data-sort="codigo">Código <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="data_emissao">Emissão <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="area">Área <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="emissor_relato">Emissor <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="indicado_analise">Indicado para análise <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="status_tratativa">Tratativa <span class="ncf-sort-icon">↕</span></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -421,17 +429,25 @@
|
||||
<span class="ncf-count" id="ncf-analise-sem-acao-count"></span>
|
||||
</div>
|
||||
<p class="ncf-section__hint">Análise já registrada, mas nenhuma ação foi aberta ainda.</p>
|
||||
<div class="ncf-filtro-grupo">
|
||||
<span class="ncf-filtro-grupo__label">Tratativa:</span>
|
||||
<div class="ncf-chip-row" id="ncf-filtro-tratativa-analise-sem-acao">
|
||||
<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">
|
||||
<table class="pa-table ncf-table-sortable" id="ncf-table-analise-sem-acao">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Código</th>
|
||||
<th>Emissão</th>
|
||||
<th>Área</th>
|
||||
<th>Responsável pela análise</th>
|
||||
<th>Descrição da análise</th>
|
||||
<th>Tratativa</th>
|
||||
<th data-sort="codigo">Código <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="data_emissao">Emissão <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="area">Área <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="responsavel_analise">Responsável pela análise <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="descricao_analise">Descrição da análise <span class="ncf-sort-icon">↕</span></th>
|
||||
<th data-sort="status_tratativa">Tratativa <span class="ncf-sort-icon">↕</span></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -449,7 +465,8 @@
|
||||
<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="todas">Todas</button>
|
||||
<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>
|
||||
@ -467,18 +484,51 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pa-table-wrap">
|
||||
<table class="pa-table">
|
||||
<table class="pa-table ncf-table-sortable" id="ncf-table-acoes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Ocorrência/Ação</th>
|
||||
<th>Área</th>
|
||||
<th>Tipo</th>
|
||||
<th>Ação</th>
|
||||
<th>Responsável</th>
|
||||
<th>Vencimento</th>
|
||||
<th>Prazo</th>
|
||||
<th>Tratativa</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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user