1328 lines
58 KiB
JavaScript
1328 lines
58 KiB
JavaScript
/**
|
|
* Controlador de "Não Conformidades" (Relatórios > Qualidade) — gestão
|
|
* contínua das ocorrências/ações do Sigsistem, ver CLAUDE.md do pacote
|
|
* `portal_api/nao_conformidades/`. Guarda de página no mesmo padrão de
|
|
* `importacao-plano-saude.js`/`indicador-desempenho.js`: retorna cedo se o
|
|
* elemento raiz da tela não existir (script incluído em todo shell) e some
|
|
* atrás de `#ncf-no-access` se o usuário não tiver
|
|
* `permissoes_efetivas.relatorios.apps["nao-conformidades"]`.
|
|
*/
|
|
|
|
function pidNcfFormatDate(iso) {
|
|
if (!iso) return "—";
|
|
const [ano, mes, dia] = iso.split("-");
|
|
return `${dia}/${mes}/${ano}`;
|
|
}
|
|
|
|
function pidNcfFormatDateTime(iso) {
|
|
if (!iso) return "—";
|
|
const data = new Date(iso);
|
|
if (Number.isNaN(data.getTime())) return "—";
|
|
return data.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
}
|
|
|
|
// Delegado ao `pidEscapeHtml` (api.js), que também escapa aspas (a versão
|
|
// antiga por `textContent` não escapava, e a função é usada em `value="..."`).
|
|
function pidNcfEscapeHtml(texto) {
|
|
return pidEscapeHtml(texto);
|
|
}
|
|
|
|
function pidNcfAtualizarContador(el, quantidade) {
|
|
el.textContent = quantidade ? `${quantidade}` : "";
|
|
el.hidden = quantidade === 0;
|
|
}
|
|
|
|
const PID_NCF_STATUS_PRAZO_CLASSES = {
|
|
finalizada: "status-pill status-pill--ok",
|
|
vencida: "status-pill status-pill--danger",
|
|
vence_7_dias: "status-pill status-pill--warning",
|
|
vence_30_dias: "status-pill status-pill--neutral",
|
|
no_prazo: "status-pill status-pill--ok",
|
|
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
|
|
* que a linha é aberta (lazy). */
|
|
function pidNcfCriarLinhaExpandivel(colspan, montarConteudo) {
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = "ncf-expand-btn";
|
|
btn.setAttribute("aria-expanded", "false");
|
|
btn.setAttribute("aria-label", "Expandir detalhes");
|
|
btn.innerHTML =
|
|
'<svg class="ncf-expand-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
|
|
const trDetalhe = document.createElement("tr");
|
|
trDetalhe.className = "ncf-detail-row";
|
|
trDetalhe.hidden = true;
|
|
const td = document.createElement("td");
|
|
td.colSpan = colspan;
|
|
// Classe própria (não só .ncf-detail-row td) pra vencer .pa-table
|
|
// td:last-child (text-align:right) — como esta é a única célula da linha,
|
|
// ela sempre "é" a última.
|
|
td.className = "ncf-detail-cell";
|
|
const painel = document.createElement("div");
|
|
painel.className = "ncf-detail-panel";
|
|
td.appendChild(painel);
|
|
trDetalhe.appendChild(td);
|
|
|
|
btn.addEventListener("click", async () => {
|
|
const aberto = btn.getAttribute("aria-expanded") === "true";
|
|
btn.setAttribute("aria-expanded", String(!aberto));
|
|
trDetalhe.hidden = aberto;
|
|
if (!aberto && !painel.dataset.carregado) {
|
|
painel.dataset.carregado = "1";
|
|
await montarConteudo(painel);
|
|
}
|
|
});
|
|
|
|
return { btn, trDetalhe };
|
|
}
|
|
|
|
function pidNcfCriarBadgeTratativa(item) {
|
|
const fragmento = document.createDocumentFragment();
|
|
const tratado = item.status_tratativa === "tratado";
|
|
const badge = document.createElement("span");
|
|
badge.className = `status-pill ${tratado ? "status-pill--ok" : "status-pill--neutral"}`;
|
|
badge.textContent = tratado ? "Tratado" : "Pendente";
|
|
fragmento.appendChild(badge);
|
|
if (item.reaberto_motivo) {
|
|
const reaberto = document.createElement("span");
|
|
reaberto.className = "ncf-reaberto";
|
|
reaberto.style.marginLeft = "6px";
|
|
reaberto.title = item.reaberto_motivo;
|
|
reaberto.textContent = "Reaberto";
|
|
fragmento.appendChild(reaberto);
|
|
}
|
|
return fragmento;
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
const contentEl = document.getElementById("ncf-content");
|
|
if (!contentEl) return;
|
|
|
|
const me = await pidGetMe();
|
|
if (!me) return;
|
|
|
|
const relatoriosPerms = (me.permissoes_efetivas && me.permissoes_efetivas["relatorios"]) || {};
|
|
const canUse = !!(relatoriosPerms.apps && relatoriosPerms.apps["nao-conformidades"]);
|
|
|
|
const noAccessEl = document.getElementById("ncf-no-access");
|
|
if (noAccessEl) noAccessEl.hidden = canUse;
|
|
contentEl.hidden = !canUse;
|
|
if (!canUse) return;
|
|
|
|
// ---------------- Tabs ----------------
|
|
const tabButtons = Array.from(document.querySelectorAll("[data-ncf-tab]"));
|
|
const tabPanels = Array.from(document.querySelectorAll("[data-ncf-panel]"));
|
|
let gestaoCarregada = false;
|
|
let importarCarregado = false;
|
|
|
|
// Só troca a UI — nunca dispara carregamento de dados sozinha. Isso evita
|
|
// uma corrida real que existiu aqui: um drill-down do Dashboard chamava
|
|
// `ativarTab("gestao")` (que carregava a Gestão com os filtros ainda
|
|
// default) E, na sequência, também recarregava com o filtro escolhido —
|
|
// as duas chamadas concorrentes escreviam em `acoesTodos`, e a que
|
|
// terminasse por último vencia (às vezes a errada, mostrando "Todas" em
|
|
// vez do filtro clicado). Quem precisa de dados chama
|
|
// `garantirGestaoCarregada()`/`carregarAcoes()` explicitamente, sempre
|
|
// com `await`, nunca em paralelo com outra chamada à mesma lista.
|
|
function ativarTab(nome) {
|
|
tabButtons.forEach((btn) => btn.classList.toggle("is-active", btn.dataset.ncfTab === nome));
|
|
tabPanels.forEach((panel) => {
|
|
panel.hidden = panel.dataset.ncfPanel !== nome;
|
|
});
|
|
}
|
|
|
|
tabButtons.forEach((btn) =>
|
|
btn.addEventListener("click", async () => {
|
|
const nome = btn.dataset.ncfTab;
|
|
ativarTab(nome);
|
|
if (nome === "gestao") await garantirGestaoCarregada();
|
|
if (nome === "importar" && !importarCarregado) {
|
|
importarCarregado = true;
|
|
await carregarImportacoes();
|
|
}
|
|
})
|
|
);
|
|
|
|
// ---------------- Estado compartilhado da Gestão ----------------
|
|
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);
|
|
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 = "";
|
|
|
|
// 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");
|
|
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();
|
|
if (!termo) return itens;
|
|
return itens.filter((item) =>
|
|
campos.some((campo) => (item[campo] || "").toString().toLowerCase().includes(termo))
|
|
);
|
|
}
|
|
|
|
function renderTudoGestao() {
|
|
renderSemAnalise();
|
|
renderAnaliseSemAcao();
|
|
renderAcoes();
|
|
}
|
|
|
|
function aplicarBusca(valor) {
|
|
buscaGestao = valor;
|
|
gestaoSearch.value = valor;
|
|
gestaoSearchClearBtn.hidden = !valor;
|
|
renderTudoGestao();
|
|
}
|
|
|
|
gestaoSearch.addEventListener("input", () => {
|
|
aplicarBusca(gestaoSearch.value);
|
|
});
|
|
gestaoSearchClearBtn.addEventListener("click", () => aplicarBusca(""));
|
|
|
|
function atualizarChipsAtivos(container, datasetAttr, valor) {
|
|
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);
|
|
carregarAcoes();
|
|
});
|
|
filtroTratativaEl.addEventListener("click", (evento) => {
|
|
const btn = evento.target.closest("[data-ncf-tratativa]");
|
|
if (!btn) return;
|
|
tratativaFiltroAtual = btn.dataset.ncfTratativa;
|
|
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;
|
|
gestaoCarregada = true;
|
|
await Promise.all([carregarSemAnalise(), carregarAnaliseSemAcao(), carregarAcoes()]);
|
|
}
|
|
|
|
// ---------------- Gestão: Ocorrências sem análise ----------------
|
|
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() {
|
|
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() {
|
|
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);
|
|
semAnaliseItensVisiveis = itens;
|
|
atualizarResumoGestao();
|
|
pidNcfAtualizarContador(semAnaliseCount, itens.length);
|
|
semAnaliseBody.innerHTML = "";
|
|
semAnaliseEmpty.hidden = itens.length > 0;
|
|
itens.forEach((oc) => {
|
|
const tratado = oc.status_tratativa === "tratado";
|
|
const { btn, trDetalhe } = pidNcfCriarLinhaExpandivel(8, (painel) => montarPainelOcorrencia(painel, oc));
|
|
|
|
const tr = document.createElement("tr");
|
|
const tdBtn = document.createElement("td");
|
|
tdBtn.className = "ncf-expand-col";
|
|
tdBtn.appendChild(btn);
|
|
tr.appendChild(tdBtn);
|
|
tr.insertAdjacentHTML(
|
|
"beforeend",
|
|
`<td>${pidNcfEscapeHtml(oc.codigo)}</td>
|
|
<td>${pidNcfFormatDate(oc.data_emissao)}</td>
|
|
<td>${pidNcfEscapeHtml(oc.area) || "—"}</td>
|
|
<td>${pidNcfEscapeHtml(oc.emissor_relato) || "—"}</td>
|
|
<td>${pidNcfEscapeHtml(oc.indicado_analise) || "—"}</td>`
|
|
);
|
|
const tdTratativa = document.createElement("td");
|
|
tdTratativa.appendChild(pidNcfCriarBadgeTratativa(oc));
|
|
tr.appendChild(tdTratativa);
|
|
const tdAcao = document.createElement("td");
|
|
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", () => alternarTratativaOcorrencia(oc.id, tratado));
|
|
tdAcao.appendChild(tratarBtn);
|
|
tr.appendChild(tdAcao);
|
|
|
|
semAnaliseBody.appendChild(tr);
|
|
semAnaliseBody.appendChild(trDetalhe);
|
|
});
|
|
}
|
|
|
|
// ---------------- Gestão: Análise sem ação ----------------
|
|
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() {
|
|
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() {
|
|
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);
|
|
analiseSemAcaoItensVisiveis = itens;
|
|
atualizarResumoGestao();
|
|
pidNcfAtualizarContador(analiseSemAcaoCount, itens.length);
|
|
analiseSemAcaoBody.innerHTML = "";
|
|
analiseSemAcaoEmpty.hidden = itens.length > 0;
|
|
itens.forEach((oc) => {
|
|
const tratado = oc.status_tratativa === "tratado";
|
|
const descricao = (oc.descricao_analise || "").slice(0, 140);
|
|
const { btn, trDetalhe } = pidNcfCriarLinhaExpandivel(8, (painel) => montarPainelOcorrencia(painel, oc));
|
|
|
|
const tr = document.createElement("tr");
|
|
const tdBtn = document.createElement("td");
|
|
tdBtn.className = "ncf-expand-col";
|
|
tdBtn.appendChild(btn);
|
|
tr.appendChild(tdBtn);
|
|
tr.insertAdjacentHTML(
|
|
"beforeend",
|
|
`<td>${pidNcfEscapeHtml(oc.codigo)}</td>
|
|
<td>${pidNcfFormatDate(oc.data_emissao)}</td>
|
|
<td>${pidNcfEscapeHtml(oc.area) || "—"}</td>
|
|
<td>${pidNcfEscapeHtml(oc.responsavel_analise) || "—"}</td>
|
|
<td>${pidNcfEscapeHtml(descricao)}${(oc.descricao_analise || "").length > 140 ? "…" : ""}</td>`
|
|
);
|
|
const tdTratativa = document.createElement("td");
|
|
tdTratativa.appendChild(pidNcfCriarBadgeTratativa(oc));
|
|
tr.appendChild(tdTratativa);
|
|
const tdAcao = document.createElement("td");
|
|
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", () => alternarTratativaOcorrencia(oc.id, tratado));
|
|
tdAcao.appendChild(tratarBtn);
|
|
tr.appendChild(tdAcao);
|
|
|
|
analiseSemAcaoBody.appendChild(tr);
|
|
analiseSemAcaoBody.appendChild(trDetalhe);
|
|
});
|
|
}
|
|
|
|
async function alternarTratativaOcorrencia(id, estaTratado) {
|
|
const acao = estaTratado ? "reabrir" : "marcar-tratado";
|
|
try {
|
|
await pidApiRequest(`/nao-conformidades/ocorrencias/${id}/${acao}/`, { method: "POST" });
|
|
await Promise.all([carregarSemAnalise(), carregarAnaliseSemAcao()]);
|
|
} catch (e) {
|
|
await pidAlert(e.message || "Não foi possível atualizar o status.");
|
|
}
|
|
}
|
|
|
|
// ---------------- Painel de detalhe de uma ocorrência (lazy) ----------------
|
|
async function montarPainelOcorrencia(painel, item) {
|
|
const idExtra = `ncf-extra-oc-${item.id}`;
|
|
painel.innerHTML = `
|
|
<div class="ncf-detail-grid">
|
|
<div><span class="ncf-detail-field__label">Assunto</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(item.assunto) || "—"}</span></div>
|
|
<div><span class="ncf-detail-field__label">Tipo</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(item.tipo_ocorrencia) || "—"}</span></div>
|
|
<div><span class="ncf-detail-field__label">Área / Setor</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(item.area) || "—"} / ${pidNcfEscapeHtml(item.setor) || "—"}</span></div>
|
|
<div><span class="ncf-detail-field__label">Cliente(s)</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(item.clientes_relacionados) || "—"}</span></div>
|
|
<div><span class="ncf-detail-field__label">Tipo(s) de causa</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(item.tipos_causa) || "—"}</span></div>
|
|
<div><span class="ncf-detail-field__label">Emissor</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(item.emissor_relato) || "—"}</span></div>
|
|
</div>
|
|
<h4>Descrição da análise</h4>
|
|
<p class="ncf-detail-panel__texto">${pidNcfEscapeHtml(item.descricao_analise) || "(sem análise registrada)"}</p>
|
|
<div id="${idExtra}"><p class="ncf-detail-panel__loading">Carregando mais detalhes...</p></div>
|
|
`;
|
|
const extra = painel.querySelector(`#${idExtra}`);
|
|
try {
|
|
const detalhe = await pidApiRequest(`/nao-conformidades/ocorrencias/${item.id}/`);
|
|
const acoesHtml = (detalhe.acoes || []).length
|
|
? `<h4>Ações vinculadas</h4><ul class="ncf-detail-lista">${detalhe.acoes
|
|
.map((a) => `<li>${pidNcfEscapeHtml(a.ocorrencia_codigo)}/${pidNcfEscapeHtml(a.codigo)} — ${pidNcfEscapeHtml((a.acao_texto || "").slice(0, 80))}</li>`)
|
|
.join("")}</ul>`
|
|
: "";
|
|
extra.innerHTML = `
|
|
<div class="ncf-detail-grid">
|
|
<div><span class="ncf-detail-field__label">Origem</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(detalhe.origem) || "—"}</span></div>
|
|
<div><span class="ncf-detail-field__label">Data do relato</span><span class="ncf-detail-field__value">${pidNcfFormatDate(detalhe.data_relato)}</span></div>
|
|
<div><span class="ncf-detail-field__label">Prazo pra finalizar</span><span class="ncf-detail-field__value">${pidNcfFormatDate(detalhe.prazo_finalizar)}</span></div>
|
|
<div><span class="ncf-detail-field__label">Pessoas relacionadas</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(detalhe.pessoas_relacionadas) || "—"}</span></div>
|
|
<div><span class="ncf-detail-field__label">Fornecedores relacionados</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(detalhe.fornecedores_relacionados) || "—"}</span></div>
|
|
<div><span class="ncf-detail-field__label">Riscos relacionados</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(detalhe.riscos_relacionados) || "—"}</span></div>
|
|
<div><span class="ncf-detail-field__label">Representante/Gerente</span><span class="ncf-detail-field__value">${pidNcfEscapeHtml(detalhe.representante_gerente) || "—"}</span></div>
|
|
</div>
|
|
<h4>Relato</h4>
|
|
<p class="ncf-detail-panel__texto">${pidNcfEscapeHtml(detalhe.relato) || "—"}</p>
|
|
${acoesHtml}
|
|
`;
|
|
} catch (e) {
|
|
extra.innerHTML = `<p class="modal-error">${pidNcfEscapeHtml(e.message || "Erro ao carregar detalhes.")}</p>`;
|
|
}
|
|
}
|
|
|
|
// ---------------- Gestão: Ações ----------------
|
|
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() {
|
|
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 (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() {
|
|
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);
|
|
acoesItensVisiveis = itens;
|
|
atualizarResumoGestao();
|
|
pidNcfAtualizarContador(acoesCount, itens.length);
|
|
acoesBody.innerHTML = "";
|
|
acoesEmpty.hidden = itens.length > 0;
|
|
itens.forEach((acaoItem) => {
|
|
const tratado = acaoItem.status_tratativa === "tratado";
|
|
// 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>${pidNcfEscapeHtml(acaoItem.ocorrencia_codigo)}/${pidNcfEscapeHtml(acaoItem.codigo)}</td>
|
|
<td>${pidNcfEscapeHtml(acaoItem.ocorrencia_area) || "—"}</td>
|
|
<td>${pidNcfEscapeHtml(acaoItem.tipo_acao) || "—"}</td>
|
|
<td>${pidNcfEscapeHtml(acaoItem.ocorrencia_tipo_ocorrencia) || "—"}</td>
|
|
<td>${pidNcfEscapeHtml(acaoItem.executor) || "—"}</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");
|
|
badgePrazo.className = PID_NCF_STATUS_PRAZO_CLASSES[acaoItem.status_prazo] || "status-pill status-pill--neutral";
|
|
badgePrazo.textContent = acaoItem.status_prazo_label;
|
|
tdPrazo.appendChild(badgePrazo);
|
|
tr.appendChild(tdPrazo);
|
|
|
|
const tdTratativa = document.createElement("td");
|
|
tdTratativa.appendChild(pidNcfCriarBadgeTratativa(acaoItem));
|
|
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);
|
|
});
|
|
}
|
|
|
|
async function alternarTratativaAcao(id, estaTratado) {
|
|
const acao = estaTratado ? "reabrir" : "marcar-tratado";
|
|
try {
|
|
await pidApiRequest(`/nao-conformidades/acoes/${id}/${acao}/`, { method: "POST" });
|
|
await carregarAcoes();
|
|
} catch (e) {
|
|
await pidAlert(e.message || "Não foi possível atualizar o status.");
|
|
}
|
|
}
|
|
|
|
// ---------------- Painel de detalhe de uma ação (lazy) — histórico ----------------
|
|
async function montarPainelAcao(painel, item) {
|
|
const idAcomp = `ncf-acomp-${item.id}`;
|
|
const finalizadaHtml = item.data_finalizacao
|
|
? `<p class="ncf-detail-panel__texto"><strong>Finalizada em:</strong> ${pidNcfFormatDate(item.data_finalizacao)}</p>`
|
|
: "";
|
|
painel.innerHTML = `
|
|
<p class="ncf-detail-panel__texto"><strong>Ação:</strong> ${pidNcfEscapeHtml(item.acao_texto) || "—"}</p>
|
|
${finalizadaHtml}
|
|
<h4>Histórico de acompanhamentos</h4>
|
|
<div id="${idAcomp}"><p class="ncf-detail-panel__loading">Carregando...</p></div>
|
|
`;
|
|
const container = painel.querySelector(`#${idAcomp}`);
|
|
try {
|
|
const detalhe = await pidApiRequest(`/nao-conformidades/acoes/${item.id}/`);
|
|
const entradas = (detalhe.acompanhamentos || []).slice().sort((a, b) => b.data.localeCompare(a.data));
|
|
if (!entradas.length) {
|
|
container.innerHTML = "<p>Nenhum acompanhamento registrado.</p>";
|
|
return;
|
|
}
|
|
container.innerHTML = "";
|
|
const lista = document.createElement("div");
|
|
lista.className = "ncf-acompanhamentos-list";
|
|
container.appendChild(lista);
|
|
|
|
function renderizarEntrada(entrada) {
|
|
const el = document.createElement("div");
|
|
el.className = "ncf-acompanhamento-item";
|
|
el.innerHTML = `
|
|
<div class="ncf-acompanhamento-item__header">
|
|
<strong>${pidNcfFormatDate(entrada.data)}</strong>
|
|
<span>${pidNcfEscapeHtml(entrada.autor) || "—"}</span>
|
|
${entrada.eh_prorrogacao ? '<span class="status-pill status-pill--warning">Prorrogação</span>' : ""}
|
|
</div>
|
|
<p></p>
|
|
`;
|
|
el.querySelector("p").textContent = entrada.texto;
|
|
lista.appendChild(el);
|
|
}
|
|
|
|
entradas.slice(0, 3).forEach(renderizarEntrada);
|
|
if (entradas.length > 3) {
|
|
const maisBtn = document.createElement("button");
|
|
maisBtn.type = "button";
|
|
maisBtn.className = "btn-outline ncf-mostrar-mais-btn";
|
|
maisBtn.textContent = `Mostrar mais (${entradas.length - 3})`;
|
|
maisBtn.addEventListener("click", () => {
|
|
entradas.slice(3).forEach(renderizarEntrada);
|
|
maisBtn.remove();
|
|
});
|
|
container.appendChild(maisBtn);
|
|
}
|
|
} catch (e) {
|
|
container.innerHTML = `<p class="modal-error">${pidNcfEscapeHtml(e.message || "Erro ao carregar histórico.")}</p>`;
|
|
}
|
|
}
|
|
|
|
// ---------------- 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"));
|
|
// 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) => {
|
|
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") || evento.target.closest(".ncf-resumo-wrap")) 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());
|
|
|
|
// ---------------- 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));
|
|
});
|
|
|
|
function pidNcfCopiarTextoFallback(texto) {
|
|
const textarea = document.createElement("textarea");
|
|
textarea.value = texto;
|
|
textarea.style.position = "fixed";
|
|
textarea.style.top = "-1000px";
|
|
textarea.style.left = "-1000px";
|
|
document.body.appendChild(textarea);
|
|
textarea.focus();
|
|
textarea.select();
|
|
let ok = false;
|
|
try {
|
|
ok = document.execCommand("copy");
|
|
} catch (e) {
|
|
ok = false;
|
|
}
|
|
document.body.removeChild(textarea);
|
|
return ok;
|
|
}
|
|
|
|
async function pidNcfCopiarTexto(texto) {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(texto);
|
|
return true;
|
|
}
|
|
return pidNcfCopiarTextoFallback(texto);
|
|
}
|
|
|
|
resumoCopiarBtn.addEventListener("click", async () => {
|
|
try {
|
|
const copiado = await pidNcfCopiarTexto(resumoTextoAtual);
|
|
if (!copiado) throw new Error("copy fallback failed");
|
|
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");
|
|
await garantirGestaoCarregada();
|
|
aplicarBusca(valor);
|
|
}
|
|
|
|
async function irParaSecaoGestao(sectionId) {
|
|
ativarTab("gestao");
|
|
await garantirGestaoCarregada();
|
|
const el = document.getElementById(sectionId);
|
|
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
}
|
|
|
|
async function irParaAcoesComPrazo(bucket) {
|
|
// 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]);
|
|
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 = "";
|
|
atualizarChipsMultiAtivos(filtroPrazoEl, "ncfPrazo", prazoFiltrosSelecionados);
|
|
atualizarChipsAtivos(filtroTratativaEl, "ncfTratativa", "todas");
|
|
ativarTab("gestao");
|
|
if (gestaoCarregada) {
|
|
await carregarAcoes();
|
|
} else {
|
|
await garantirGestaoCarregada();
|
|
}
|
|
const el = document.getElementById("ncf-section-acoes");
|
|
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
}
|
|
|
|
// ---------------- 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");
|
|
const periodoChipsEl = document.getElementById("ncf-dash-periodo-chips");
|
|
let periodoAtual = "6";
|
|
|
|
periodoChipsEl.addEventListener("click", (evento) => {
|
|
const btn = evento.target.closest("[data-ncf-periodo]");
|
|
if (!btn) return;
|
|
periodoAtual = btn.dataset.ncfPeriodo;
|
|
atualizarChipsAtivos(periodoChipsEl, "ncfPeriodo", periodoAtual);
|
|
carregarDashboard();
|
|
});
|
|
|
|
function preencheRankingMotivos(tbody, itens) {
|
|
tbody.innerHTML = "";
|
|
if (!itens.length) {
|
|
tbody.innerHTML = '<tr><td colspan="2">Nenhum registro no período.</td></tr>';
|
|
return;
|
|
}
|
|
itens.forEach((item) => {
|
|
const tr = document.createElement("tr");
|
|
tr.className = "ncf-clicavel";
|
|
tr.innerHTML = `<td>${pidNcfEscapeHtml(item.causa)}</td><td>${item.quantidade}</td>`;
|
|
tr.addEventListener("click", () => irParaGestaoComBusca(item.causa));
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function preencheRankingTipo(tbody, itens, chaveNome) {
|
|
tbody.innerHTML = "";
|
|
if (!itens.length) {
|
|
tbody.innerHTML = '<tr><td colspan="5">Nenhum registro no período.</td></tr>';
|
|
return;
|
|
}
|
|
itens.forEach((item) => {
|
|
const tr = document.createElement("tr");
|
|
tr.className = "ncf-clicavel";
|
|
tr.innerHTML = `
|
|
<td>${pidNcfEscapeHtml(item[chaveNome])}</td>
|
|
<td class="ncf-num--nc">${item.nc}</td>
|
|
<td class="ncf-num--reclamacao">${item.reclamacao}</td>
|
|
<td>${item.outros}</td>
|
|
<td class="ncf-num--total">${item.total}</td>
|
|
`;
|
|
tr.addEventListener("click", () => irParaGestaoComBusca(item[chaveNome]));
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function pidNcfFormatDias(valor) {
|
|
if (valor == null) return null;
|
|
return valor.toString().replace(".", ",");
|
|
}
|
|
|
|
async function carregarDashboard() {
|
|
const dados = await pidApiRequest(`/nao-conformidades/dashboard/?meses=${encodeURIComponent(periodoAtual)}`);
|
|
// "Ações abertas" exclui quem já foi finalizado no Sigsistem — uma ação
|
|
// concluída há anos não é mais um item de trabalho em aberto, mesmo que
|
|
// o prazo dela (congelado) já tenha passado há muito tempo.
|
|
const statusPorPrazo = dados.acoes_abertas_por_status || {};
|
|
const totalFinalizadas = statusPorPrazo.finalizada || 0;
|
|
const totalAcoes = Object.entries(statusPorPrazo).reduce(
|
|
(soma, [chave, valor]) => (chave === "finalizada" ? soma : soma + valor),
|
|
0
|
|
);
|
|
|
|
const cartoes = [
|
|
{ rotulo: "Ocorrências abertas", valor: dados.ocorrencias_abertas, tom: "" },
|
|
{ rotulo: "Ações abertas", valor: totalAcoes, tom: "" },
|
|
{
|
|
rotulo: "Ações finalizadas",
|
|
valor: totalFinalizadas,
|
|
tom: "",
|
|
acao: () => irParaAcoesComPrazo("finalizada"),
|
|
},
|
|
{
|
|
rotulo: "Ações vencidas",
|
|
valor: statusPorPrazo.vencida || 0,
|
|
tom: "ncf-card--danger",
|
|
acao: () => irParaAcoesComPrazo("vencida"),
|
|
},
|
|
{
|
|
rotulo: "Vencem em ≤7 dias",
|
|
valor: statusPorPrazo.vence_7_dias || 0,
|
|
tom: "ncf-card--warning",
|
|
acao: () => irParaAcoesComPrazo("vence_7_dias"),
|
|
},
|
|
{
|
|
rotulo: "Ocorrências sem análise",
|
|
valor: dados.sem_analise,
|
|
tom: "ncf-card--warning",
|
|
acao: () => irParaSecaoGestao("ncf-section-sem-analise"),
|
|
},
|
|
{
|
|
rotulo: "Análises sem ação",
|
|
valor: dados.analise_sem_acao,
|
|
tom: "ncf-card--warning",
|
|
acao: () => irParaSecaoGestao("ncf-section-analise-sem-acao"),
|
|
},
|
|
{ rotulo: "NCs sem ação corretiva", valor: dados.ncs_sem_acao_corretiva, tom: "ncf-card--danger" },
|
|
];
|
|
dashCards.innerHTML = "";
|
|
cartoes.forEach((cartao) => {
|
|
const el = document.createElement(cartao.acao ? "button" : "div");
|
|
el.className = cartao.acao ? `ncf-card ncf-card--clicavel ${cartao.tom}` : `ncf-card ${cartao.tom}`;
|
|
if (cartao.acao) {
|
|
el.type = "button";
|
|
el.addEventListener("click", cartao.acao);
|
|
}
|
|
el.innerHTML = `<span class="ncf-card__label">${cartao.rotulo}</span><span class="ncf-card__value">${cartao.valor}</span>`;
|
|
dashCards.appendChild(el);
|
|
});
|
|
|
|
const tempos = dados.tempos_medios || {};
|
|
const kpis = [
|
|
{
|
|
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) => {
|
|
const el = document.createElement("div");
|
|
el.className = "ncf-kpi";
|
|
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__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 linhaHtml = (item) =>
|
|
`<tr><td>${pidNcfEscapeHtml(item.categoria)}</td><td>${pidNcfFormatDias(item.media_dias)} dias</td><td>${item.n}</td></tr>`;
|
|
// Duas colunas lado a lado em vez de uma lista comprida — metade das
|
|
// categorias em cada uma (pedido do usuário: "4 de cada lado").
|
|
const metade = Math.ceil(execucaoPorCategoria.length / 2);
|
|
const colunas = [execucaoPorCategoria.slice(0, metade), execucaoPorCategoria.slice(metade)];
|
|
const tabelaHtml = (itens) =>
|
|
itens.length
|
|
? `<div class="pa-table-wrap"><table class="pa-table">
|
|
<thead><tr><th>Categoria</th><th>Média</th><th>Qtd.</th></tr></thead>
|
|
<tbody>${itens.map(linhaHtml).join("")}</tbody>
|
|
</table></div>`
|
|
: "";
|
|
dashExecucaoCategoria.innerHTML = `
|
|
<h4 class="ncf-execucao-categoria__title">Execução por categoria de ação</h4>
|
|
<div class="ncf-execucao-categoria__grid">
|
|
${colunas.map(tabelaHtml).join("")}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
preencheRankingMotivos(dashMotivosBody, dados.motivos_abertura || []);
|
|
preencheRankingTipo(dashClientesBody, dados.clientes_maior_incidencia || [], "cliente");
|
|
preencheRankingTipo(dashColaboradoresBody, dados.colaboradores_maior_incidencia || [], "colaborador");
|
|
}
|
|
|
|
// ---------------- Importar ----------------
|
|
const importToggleBtn = document.getElementById("ncf-import-toggle-btn");
|
|
const importForm = document.getElementById("ncf-import-form");
|
|
const formOcorrencias = document.getElementById("ncf-form-ocorrencias");
|
|
const formOcorrenciasName = document.getElementById("ncf-form-ocorrencias-name");
|
|
const formAcoes = document.getElementById("ncf-form-acoes");
|
|
const formAcoesName = document.getElementById("ncf-form-acoes-name");
|
|
const formError = document.getElementById("ncf-form-error");
|
|
const formCancelBtn = document.getElementById("ncf-form-cancel-btn");
|
|
const formSubmitBtn = document.getElementById("ncf-form-submit-btn");
|
|
const importResultado = document.getElementById("ncf-import-resultado");
|
|
const importListBody = document.getElementById("ncf-import-list-body");
|
|
const importListEmpty = document.getElementById("ncf-import-list-empty");
|
|
|
|
importToggleBtn.addEventListener("click", () => {
|
|
importForm.hidden = !importForm.hidden;
|
|
});
|
|
formCancelBtn.addEventListener("click", () => {
|
|
importForm.hidden = true;
|
|
formError.textContent = "";
|
|
});
|
|
formOcorrencias.addEventListener("change", () => {
|
|
formOcorrenciasName.textContent = formOcorrencias.files[0]?.name || "Nenhum arquivo selecionado";
|
|
});
|
|
formAcoes.addEventListener("change", () => {
|
|
formAcoesName.textContent = formAcoes.files[0]?.name || "Nenhum arquivo selecionado";
|
|
});
|
|
|
|
formSubmitBtn.addEventListener("click", async () => {
|
|
formError.textContent = "";
|
|
if (!formOcorrencias.files[0] || !formAcoes.files[0]) {
|
|
formError.textContent = "Anexe os dois arquivos antes de processar.";
|
|
return;
|
|
}
|
|
const dados = new FormData();
|
|
dados.append("arquivo_ocorrencias", formOcorrencias.files[0]);
|
|
dados.append("arquivo_acoes", formAcoes.files[0]);
|
|
|
|
formSubmitBtn.disabled = true;
|
|
formSubmitBtn.textContent = "Processando...";
|
|
try {
|
|
const importacao = await pidApiRequest("/nao-conformidades/importacoes/", { method: "POST", body: dados });
|
|
importForm.hidden = true;
|
|
formOcorrencias.value = "";
|
|
formAcoes.value = "";
|
|
formOcorrenciasName.textContent = "Nenhum arquivo selecionado";
|
|
formAcoesName.textContent = "Nenhum arquivo selecionado";
|
|
mostrarResultadoImportacao(importacao);
|
|
await carregarImportacoes();
|
|
gestaoCarregada = false;
|
|
await garantirGestaoCarregada();
|
|
await carregarDashboard();
|
|
} catch (e) {
|
|
formError.textContent = e.message || "Não foi possível processar a importação.";
|
|
} finally {
|
|
formSubmitBtn.disabled = false;
|
|
formSubmitBtn.textContent = "Processar";
|
|
}
|
|
});
|
|
|
|
function mostrarResultadoImportacao(importacao) {
|
|
const resumo = importacao.resumo || {};
|
|
const avisos = importacao.avisos || [];
|
|
const partes = [
|
|
`${resumo.ocorrencias_novas || 0} ocorrência(s) nova(s), ${resumo.ocorrencias_atualizadas || 0} atualizada(s)`,
|
|
`${resumo.acoes_novas || 0} ação(ões) nova(s), ${resumo.acoes_atualizadas || 0} atualizada(s)`,
|
|
`${resumo.acompanhamentos_novos || 0} acompanhamento(s) novo(s)`,
|
|
`${(resumo.itens_reabertos || []).length} item(ns) reaberto(s) automaticamente`,
|
|
];
|
|
let texto = "Importação processada: " + partes.join(" · ") + ".";
|
|
if (avisos.length) texto += " Avisos: " + avisos.join(" ");
|
|
importResultado.textContent = texto;
|
|
importResultado.hidden = false;
|
|
}
|
|
|
|
async function carregarImportacoes() {
|
|
const itens = await pidApiRequest("/nao-conformidades/importacoes/");
|
|
importListBody.innerHTML = "";
|
|
importListEmpty.hidden = itens.length > 0;
|
|
itens.forEach((importacao) => {
|
|
const resumo = importacao.resumo || {};
|
|
const avisos = importacao.avisos || [];
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML = `
|
|
<td>${pidNcfFormatDateTime(importacao.criado_em)}</td>
|
|
<td>${pidNcfEscapeHtml(importacao.criado_por_nome) || "—"}</td>
|
|
<td>${resumo.ocorrencias_novas || 0} / ${resumo.ocorrencias_atualizadas || 0}</td>
|
|
<td>${resumo.acoes_novas || 0} / ${resumo.acoes_atualizadas || 0}</td>
|
|
<td>${(resumo.itens_reabertos || []).length}</td>
|
|
<td>${avisos.length ? avisos.length + " aviso(s)" : "—"}</td>
|
|
<td></td>
|
|
`;
|
|
const acaoTd = tr.lastElementChild;
|
|
const excluirBtn = document.createElement("button");
|
|
excluirBtn.type = "button";
|
|
excluirBtn.className = "btn-danger-outline";
|
|
excluirBtn.textContent = "Excluir";
|
|
excluirBtn.addEventListener("click", async () => {
|
|
const confirmado = await pidConfirm(
|
|
"Excluir este registro de importação? As ocorrências/ações já importadas não são afetadas.",
|
|
{ perigoso: true }
|
|
);
|
|
if (!confirmado) return;
|
|
try {
|
|
await pidApiRequest(`/nao-conformidades/importacoes/${importacao.id}/`, { method: "DELETE" });
|
|
await carregarImportacoes();
|
|
} catch (e) {
|
|
await pidAlert(e.message || "Não foi possível excluir.");
|
|
}
|
|
});
|
|
acaoTd.appendChild(excluirBtn);
|
|
importListBody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
// Carga inicial — a aba Dashboard já vem visível por padrão.
|
|
await carregarDashboard();
|
|
});
|