portal_publico/static/js/nao-conformidades.js

779 lines
33 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" });
}
function pidNcfEscapeHtml(texto) {
if (texto == null) return "";
const div = document.createElement("div");
div.textContent = String(texto);
return div.innerHTML;
}
function pidNcfAtualizarContador(el, quantidade) {
el.textContent = quantidade ? `${quantidade}` : "";
el.hidden = quantidade === 0;
}
const PID_NCF_STATUS_PRAZO_CLASSES = {
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",
};
/** 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 = [];
let prazoFiltroAtual = "todas";
let tratativaFiltroAtual = "pendente";
let buscaGestao = "";
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");
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));
}
filtroPrazoEl.addEventListener("click", (evento) => {
const btn = evento.target.closest("[data-ncf-prazo]");
if (!btn) return;
prazoFiltroAtual = btn.dataset.ncfPrazo;
atualizarChipsAtivos(filtroPrazoEl, "ncfPrazo", prazoFiltroAtual);
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();
});
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");
async function carregarSemAnalise() {
semAnaliseTodos = await pidApiRequest("/nao-conformidades/ocorrencias/?sem_analise=true");
renderSemAnalise();
}
function renderSemAnalise() {
const itens = filtrarPorBusca(semAnaliseTodos, [
"indicado_analise",
"responsavel_analise",
"clientes_relacionados",
"assunto",
"emissor_relato",
]);
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>${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");
async function carregarAnaliseSemAcao() {
analiseSemAcaoTodos = await pidApiRequest("/nao-conformidades/ocorrencias/?analise_sem_acao=true");
renderAnaliseSemAcao();
}
function renderAnaliseSemAcao() {
const itens = filtrarPorBusca(analiseSemAcaoTodos, [
"indicado_analise",
"responsavel_analise",
"clientes_relacionados",
"assunto",
"descricao_analise",
]);
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>${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>${a.ocorrencia_codigo}/${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");
async function carregarAcoes() {
const params = new URLSearchParams();
if (prazoFiltroAtual !== "todas") params.set("status_prazo", prazoFiltroAtual);
if (tratativaFiltroAtual !== "todas") params.set("status_tratativa", tratativaFiltroAtual);
const query = params.toString();
acoesTodos = await pidApiRequest(`/nao-conformidades/acoes/${query ? "?" + query : ""}`);
renderAcoes();
}
function renderAcoes() {
const itens = filtrarPorBusca(acoesTodos, ["executor", "ocorrencia_clientes", "ocorrencia_codigo", "acao_texto"]);
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));
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>${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.executor) || "—"}</td>
<td>${pidNcfFormatDate(acaoItem.vencimento_efetivo)}</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");
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);
acoesBody.appendChild(tr);
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}`;
painel.innerHTML = `
<p class="ncf-detail-panel__texto"><strong>Ação:</strong> ${pidNcfEscapeHtml(item.acao_texto) || "—"}</p>
<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>`;
}
}
// ---------------- 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).
prazoFiltroAtual = bucket;
tratativaFiltroAtual = "todas";
atualizarChipsAtivos(filtroPrazoEl, "ncfPrazo", bucket);
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 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)}`);
const totalAcoes = Object.values(dados.acoes_abertas_por_status || {}).reduce((a, b) => a + b, 0);
const cartoes = [
{ rotulo: "Ocorrências abertas", valor: dados.ocorrencias_abertas, tom: "" },
{ rotulo: "Ações abertas", valor: totalAcoes, tom: "" },
{
rotulo: "Ações vencidas",
valor: dados.acoes_abertas_por_status?.vencida || 0,
tom: "ncf-card--danger",
acao: () => irParaAcoesComPrazo("vencida"),
},
{
rotulo: "Vencem em ≤7 dias",
valor: dados.acoes_abertas_por_status?.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) },
{ rotulo: "Abertura da ação (análise → ação aberta)", dias: pidNcfFormatDias(tempos.abertura_acao_dias) },
];
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>`;
el.innerHTML = `<span class="ncf-kpi__label">${kpi.rotulo}</span>${valorHtml}`;
dashKpis.appendChild(el);
});
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();
});