/** * 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 = { 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", }; /** 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 = ''; 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", `${oc.codigo} ${pidNcfFormatDate(oc.data_emissao)} ${pidNcfEscapeHtml(oc.area) || "—"} ${pidNcfEscapeHtml(oc.emissor_relato) || "—"} ${pidNcfEscapeHtml(oc.indicado_analise) || "—"}` ); 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", `${oc.codigo} ${pidNcfFormatDate(oc.data_emissao)} ${pidNcfEscapeHtml(oc.area) || "—"} ${pidNcfEscapeHtml(oc.responsavel_analise) || "—"} ${pidNcfEscapeHtml(descricao)}${(oc.descricao_analise || "").length > 140 ? "…" : ""}` ); 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 = `
Assunto${pidNcfEscapeHtml(item.assunto) || "—"}
Tipo${pidNcfEscapeHtml(item.tipo_ocorrencia) || "—"}
Área / Setor${pidNcfEscapeHtml(item.area) || "—"} / ${pidNcfEscapeHtml(item.setor) || "—"}
Cliente(s)${pidNcfEscapeHtml(item.clientes_relacionados) || "—"}
Tipo(s) de causa${pidNcfEscapeHtml(item.tipos_causa) || "—"}
Emissor${pidNcfEscapeHtml(item.emissor_relato) || "—"}

Descrição da análise

${pidNcfEscapeHtml(item.descricao_analise) || "(sem análise registrada)"}

Carregando mais detalhes...

`; const extra = painel.querySelector(`#${idExtra}`); try { const detalhe = await pidApiRequest(`/nao-conformidades/ocorrencias/${item.id}/`); const acoesHtml = (detalhe.acoes || []).length ? `

Ações vinculadas

` : ""; extra.innerHTML = `
Origem${pidNcfEscapeHtml(detalhe.origem) || "—"}
Data do relato${pidNcfFormatDate(detalhe.data_relato)}
Prazo pra finalizar${pidNcfFormatDate(detalhe.prazo_finalizar)}
Pessoas relacionadas${pidNcfEscapeHtml(detalhe.pessoas_relacionadas) || "—"}
Fornecedores relacionados${pidNcfEscapeHtml(detalhe.fornecedores_relacionados) || "—"}
Riscos relacionados${pidNcfEscapeHtml(detalhe.riscos_relacionados) || "—"}
Representante/Gerente${pidNcfEscapeHtml(detalhe.representante_gerente) || "—"}

Relato

${pidNcfEscapeHtml(detalhe.relato) || "—"}

${acoesHtml} `; } catch (e) { extra.innerHTML = ``; } } // ---------------- 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", `${acaoItem.ocorrencia_codigo}/${acaoItem.codigo} ${pidNcfEscapeHtml(acaoItem.ocorrencia_area) || "—"} ${pidNcfEscapeHtml(acaoItem.tipo_acao) || "—"} ${pidNcfEscapeHtml((acaoItem.acao_texto || "").slice(0, 90))} ${pidNcfEscapeHtml(acaoItem.executor) || "—"} ${pidNcfFormatDate(acaoItem.vencimento_efetivo)}` ); 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}`; const finalizadaHtml = item.data_finalizacao ? `

Finalizada em: ${pidNcfFormatDate(item.data_finalizacao)}

` : ""; painel.innerHTML = `

Ação: ${pidNcfEscapeHtml(item.acao_texto) || "—"}

${finalizadaHtml}

Histórico de acompanhamentos

Carregando...

`; 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 = "

Nenhum acompanhamento registrado.

"; 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 = `
${pidNcfFormatDate(entrada.data)} ${pidNcfEscapeHtml(entrada.autor) || "—"} ${entrada.eh_prorrogacao ? 'Prorrogação' : ""}

`; 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 = ``; } } // ---------------- 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 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 = 'Nenhum registro no período.'; return; } itens.forEach((item) => { const tr = document.createElement("tr"); tr.className = "ncf-clicavel"; tr.innerHTML = `${pidNcfEscapeHtml(item.causa)}${item.quantidade}`; tr.addEventListener("click", () => irParaGestaoComBusca(item.causa)); tbody.appendChild(tr); }); } function preencheRankingTipo(tbody, itens, chaveNome) { tbody.innerHTML = ""; if (!itens.length) { tbody.innerHTML = 'Nenhum registro no período.'; return; } itens.forEach((item) => { const tr = document.createElement("tr"); tr.className = "ncf-clicavel"; tr.innerHTML = ` ${pidNcfEscapeHtml(item[chaveNome])} ${item.nc} ${item.reclamacao} ${item.outros} ${item.total} `; 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 = `${cartao.rotulo}${cartao.valor}`; 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 ? 'Sem dados suficientes' : `${kpi.dias} diasn=${kpi.n}`; el.innerHTML = `${kpi.rotulo}${valorHtml}`; dashKpis.appendChild(el); }); const execucaoPorCategoria = tempos.execucao_por_categoria || []; if (!execucaoPorCategoria.length) { dashExecucaoCategoria.innerHTML = ""; } else { const linhaHtml = (item) => `${pidNcfEscapeHtml(item.categoria)}${pidNcfFormatDias(item.media_dias)} dias${item.n}`; // 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 ? `
${itens.map(linhaHtml).join("")}
CategoriaMédiaQtd.
` : ""; dashExecucaoCategoria.innerHTML = `

Execução por categoria de ação

${colunas.map(tabelaHtml).join("")}
`; } 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 = ` ${pidNcfFormatDateTime(importacao.criado_em)} ${pidNcfEscapeHtml(importacao.criado_por_nome) || "—"} ${resumo.ocorrencias_novas || 0} / ${resumo.ocorrencias_atualizadas || 0} ${resumo.acoes_novas || 0} / ${resumo.acoes_atualizadas || 0} ${(resumo.itens_reabertos || []).length} ${avisos.length ? avisos.length + " aviso(s)" : "—"} `; 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(); });