const PID_IPS_MOTIVO_LABELS = { VALOR_NEGATIVO: "Valor negativo no mês", NAO_CADASTRADO: "Não cadastrado no sistema", NOME_DIVERGENTE: "Nome divergente", TIPO_INVALIDO: "Tipo de despesa não reconhecido", }; // Espelha ImportacaoPlanoSaudeAuditoria.MOTIVOS_RESOLVIVEIS (models.py) — só // esses motivos são "problema de leitura/grafia de nome", então só eles // ganham o botão "Vincular pessoa" na aba Auditoria. const PID_IPS_MOTIVOS_RESOLVIVEIS = ["NOME_DIVERGENTE", "NAO_CADASTRADO"]; const PID_IPS_TIPO_LABELS = { mensalidade: "Mensalidade", coparticipacao: "Coparticipação", }; const PID_IPS_TIPOS = ["mensalidade", "coparticipacao"]; const PID_IPS_PESSOAS = ["titular", "dependente"]; const PID_IPS_PESSOA_LABELS = { titular: "Titular", dependente: "Dependente", }; const PID_IPS_CAMPOS_LINHA = [ ["codigo_empresa", "Cód. Empresa", 90], ["nome_func", "Nome Funcionário", 200], ["cpf_func", "CPF Funcionário", 130], ["codigo_out_emp", "Cód. Out. Emp.", 90], ["data_inicial", "Data Inicial", 100], ["nome_dependente", "Nome Dependente", 200], ["cpf_dependente", "CPF Dependente", 130], ["valor_empresa", "Valor Empresa", 110], ["valor", "Valor", 90], ["descricao", "Descrição", 240], ]; const PID_IPS_CAMPOS_AUDITORIA = [ ["nome", "Nome", 160], ["cpf", "CPF", 120], ["tipo", "Tipo", 100], ["tipo_lancamento", "Lançamento", 120], ["valor", "Valor", 100], ["motivo", "Motivo", 180], ["detalhe", "Justificativa", 320], ]; const PID_IPS_AUDITORIA_ACAO_WIDTH = 160; const PID_IPS_COL_MIN_WIDTH = 60; async function pidFetchImportacoesPlanoSaude() { return (await pidApiRequest("/importacoes-plano-saude/")) || []; } async function pidFetchImportacaoPlanoSaude(id) { return pidApiRequest(`/importacoes-plano-saude/${id}/`); } async function pidFetchOperadorasPlanoSaude() { return (await pidApiRequest("/importacoes-plano-saude/operadoras/")) || []; } async function pidExcluirImportacaoPlanoSaude(id) { return pidApiRequest(`/importacoes-plano-saude/${id}/`, { method: "DELETE" }); } async function pidCriarImportacaoPlanoSaude(formData) { return pidApiRequest("/importacoes-plano-saude/", { method: "POST", body: formData }); } async function pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey) { const formData = new FormData(); formData.append("tipo", tipo); formData.append("arquivo", arquivo); if (operadoraKey) formData.append("operadora", operadoraKey); return pidApiRequest("/importacoes-plano-saude/validar-arquivo/", { method: "POST", body: formData }); } async function pidAtualizarLinhaPlanoSaude(id, campos) { return pidApiRequest(`/importacoes-plano-saude-linhas/${id}/`, { method: "PATCH", body: campos }); } async function pidCriarLinhaPlanoSaude(importacaoId, tipoLancamento) { return pidApiRequest("/importacoes-plano-saude-linhas/", { method: "POST", body: { importacao: importacaoId, tipo_lancamento: tipoLancamento }, }); } async function pidExcluirLinhaPlanoSaude(id) { return pidApiRequest(`/importacoes-plano-saude-linhas/${id}/`, { method: "DELETE" }); } async function pidResolverAuditoriaPlanoSaude(itemId, linhaId) { return pidApiRequest(`/importacoes-plano-saude-auditoria/${itemId}/resolver/`, { method: "POST", body: { linha_id: linhaId }, }); } async function pidFetchRegrasCusteioPlanoSaude() { return (await pidApiRequest("/regras-custeio-plano-saude/")) || []; } async function pidCriarRegraCusteioPlanoSaude(dados) { return pidApiRequest("/regras-custeio-plano-saude/", { method: "POST", body: dados }); } async function pidAtualizarRegraCusteioPlanoSaude(id, dados) { return pidApiRequest(`/regras-custeio-plano-saude/${id}/`, { method: "PATCH", body: dados }); } async function pidExcluirRegraCusteioPlanoSaude(id) { return pidApiRequest(`/regras-custeio-plano-saude/${id}/`, { method: "DELETE" }); } /** * Resposta de /gerar/ é binária (CSV ou ZIP), não JSON — por isso não passa * por pidApiRequest. Reaproveita pidEnsureCsrfCookie/pidGetCookie/pidErrorMessageFrom * de api.js (globais na página) pra manter o mesmo tratamento de CSRF/erro. */ async function pidGerarArquivoPlanoSaude(id) { await pidEnsureCsrfCookie(); const response = await fetch(`/api/importacoes-plano-saude/${id}/gerar/`, { method: "POST", credentials: "include", headers: { "X-CSRFToken": pidGetCookie("csrftoken") || "" }, }); if (!response.ok) { let mensagem = "Não foi possível gerar o arquivo."; try { mensagem = pidErrorMessageFrom(await response.json()); } catch (e) { // resposta sem corpo JSON — mantém a mensagem genérica } throw new Error(mensagem); } const blob = await response.blob(); const disposition = response.headers.get("Content-Disposition") || ""; const match = disposition.match(/filename="?([^"]+)"?/); const filename = match ? match[1] : "importacao_plano_saude.csv"; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } document.addEventListener("DOMContentLoaded", async () => { const listView = document.getElementById("ips-list-view"); if (!listView) return; const me = await pidGetMe(); if (!me) return; const utilPerms = (me.permissoes_efetivas && me.permissoes_efetivas["utilitarios"]) || {}; const canUse = !!(utilPerms.apps && utilPerms.apps["importacao-plano-saude"]); const noAccessEl = document.getElementById("ips-no-access"); const contentEl = document.getElementById("ips-content"); if (noAccessEl) noAccessEl.hidden = canUse; if (contentEl) contentEl.hidden = !canUse; if (!canUse) return; const formView = document.getElementById("ips-form-view"); const reviewView = document.getElementById("ips-review-view"); const listBody = document.getElementById("ips-list-body"); const listEmpty = document.getElementById("ips-list-empty"); const listPagination = document.getElementById("ips-list-pagination"); const listCount = document.getElementById("ips-list-count"); const listSelectAll = document.getElementById("ips-list-select-all"); const listBulkActions = document.getElementById("ips-list-bulk-actions"); const listBulkCount = document.getElementById("ips-list-bulk-count"); const listBulkDeleteBtn = document.getElementById("ips-list-bulk-delete-btn"); const listSortHeaders = Array.from(document.querySelectorAll("#ips-list-table th[data-sort]")); const newBtn = document.getElementById("ips-new-btn"); const formOperadora = document.getElementById("ips-form-operadora"); const formPlanilha = document.getElementById("ips-form-planilha"); const formPlanilhaName = document.getElementById("ips-form-planilha-name"); const formPlanilhaRemoveBtn = document.getElementById("ips-form-planilha-remove"); const formPlanilhaStatus = document.getElementById("ips-form-planilha-status"); const formArquivo = document.getElementById("ips-form-arquivo"); const formArquivoName = document.getElementById("ips-form-arquivo-name"); const formArquivoRemoveBtn = document.getElementById("ips-form-arquivo-remove"); const formArquivoStatus = document.getElementById("ips-form-arquivo-status"); const formTipoMensalidade = document.getElementById("ips-form-tipo-mensalidade"); const formTipoCoparticipacao = document.getElementById("ips-form-tipo-coparticipacao"); const custeioMensalidade = document.getElementById("ips-custeio-mensalidade"); const custeioCoparticipacao = document.getElementById("ips-custeio-coparticipacao"); const ipsRegraCombo = document.getElementById("ips-regra-combo"); const ipsRegraSearch = document.getElementById("ips-regra-search"); const ipsRegraComboList = document.getElementById("ips-regra-combo-list"); const ipsRegraAplicarBtn = document.getElementById("ips-regra-aplicar-btn"); const ipsRegraSalvarBtn = document.getElementById("ips-regra-salvar-btn"); const ipsRegraObservacoes = document.getElementById("ips-regra-observacoes"); const regrasGerenciarBtn = document.getElementById("ips-regras-gerenciar-btn"); const regrasModal = document.getElementById("ips-regras-modal"); const regrasLista = document.getElementById("ips-regras-lista"); const regrasError = document.getElementById("ips-regras-error"); const regrasFecharBtn = document.getElementById("ips-regras-fechar-btn"); const regraSaveModal = document.getElementById("ips-regra-save-modal"); const regraSaveTitle = document.getElementById("ips-regra-save-title"); const regraSaveNome = document.getElementById("ips-regra-save-nome"); const regraSaveObservacoes = document.getElementById("ips-regra-save-observacoes"); const regraSaveAtualizarField = document.getElementById("ips-regra-save-atualizar-field"); const regraSaveAtualizar = document.getElementById("ips-regra-save-atualizar"); const regraSaveAtualizarLabel = document.getElementById("ips-regra-save-atualizar-label"); const regraSaveError = document.getElementById("ips-regra-save-error"); const regraSaveCancelBtn = document.getElementById("ips-regra-save-cancel-btn"); const regraSaveConfirmBtn = document.getElementById("ips-regra-save-confirm-btn"); const formError = document.getElementById("ips-form-error"); const formSubmitBtn = document.getElementById("ips-form-submit-btn"); const formCancelBtn = document.getElementById("ips-form-cancel-btn"); const formBackBtn = document.getElementById("ips-form-back-btn"); const reviewSubtitle = document.getElementById("ips-review-subtitle"); const reviewError = document.getElementById("ips-review-error"); const reviewBackBtn = document.getElementById("ips-review-back-btn"); const gerarBtn = document.getElementById("ips-gerar-btn"); const tabsEl = document.getElementById("ips-tabs"); const panelsEl = document.getElementById("ips-tab-panels"); const vincularModal = document.getElementById("ips-vincular-modal"); const vincularResumo = document.getElementById("ips-vincular-resumo"); const vincularSearch = document.getElementById("ips-vincular-search"); const vincularLista = document.getElementById("ips-vincular-lista"); const vincularError = document.getElementById("ips-vincular-error"); const vincularCancelBtn = document.getElementById("ips-vincular-cancel-btn"); const vincularConfirmBtn = document.getElementById("ips-vincular-confirm-btn"); let importacoes = []; let importacaoAtual = null; let activeTab = null; let listSortKey = "criado_em"; let listSortDir = -1; let listPage = 1; let selectedIds = new Set(); let currentPageIds = []; let colWidthsLinha = {}; let colWidthsAuditoria = {}; // Ordenação por coluna das duas tabelas da revisão (clicar no cabeçalho) — // mesma ideia de listSortKey/listSortDir do histórico, só que aqui é por // painel (linha × auditoria) em vez de por tela inteira; também em // memória, não persistida. As colunas em si têm ordem fixa (PID_IPS_CAMPOS_LINHA/ // PID_IPS_CAMPOS_AUDITORIA) — só as linhas são reordenáveis, por ordenação. let linhaSortKey = null; let linhaSortDir = 1; let auditoriaSortKey = null; let auditoriaSortDir = 1; let vincularItemAtual = null; let vincularLinhaSelecionada = null; let operadorasCache = []; // Banco de regras de custeio salvas (ver "Regra de custeio salva" no // formulário) — regraAplicadaId rastreia qual regra está atualmente // refletida no formulário (aplicada via select ou recém-salva), pra // "Salvar regra atual..." nascer em modo "atualizar" por padrão nesse caso. let regras = []; let regraAplicadaId = null; // Id atualmente escolhido no combobox pesquisável (#ips-regra-search) — // diferente de regraAplicadaId: este só reflete o que está selecionado // na caixa de busca; regraAplicadaId reflete o que está de fato refletido // nos campos do formulário (pode ficar desatualizado em relação ao combo // se o usuário digitar algo novo sem clicar em "Aplicar"). let regraSelecionadaId = null; // Compara valores de uma célula da tabela de revisão pra ordenação por // coluna — números no formato BR (milhar com ponto, decimal com vírgula, // ex.: "1.234,56") são comparados numericamente; qualquer outra coisa // (nome, CPF com hífen, data dd/mm/aaaa) cai pra comparação de texto. function comparaValorRevisao(a, b) { const paraNumero = (valor) => { if (valor === null || valor === undefined || valor === "") return NaN; const texto = String(valor).trim(); if (!/^-?[\d.,]+$/.test(texto)) return NaN; return Number(texto.replace(/\./g, "").replace(",", ".")); }; const an = paraNumero(a); const bn = paraNumero(b); if (!Number.isNaN(an) && !Number.isNaN(bn)) return an - bn; return String(a || "").localeCompare(String(b || ""), "pt-BR", { sensitivity: "base" }); } /** * Valida um único arquivo (planilha padrão OU arquivo da operadora) assim * que o colaborador anexa, chamando /validar-arquivo/ — mesmo parser que * seria usado no "Processar", só que sobre um upload descartável, pra * apontar exatamente qual dos dois documentos está fora do padrão antes de * precisar dos dois juntos. `ehInvalido()` (usado na validação do submit) * só conta como inválido depois que a resposta chega — enquanto pendente * ou sem arquivo, não bloqueia o envio (o create() no servidor valida de * novo, é só essa checagem antecipada que não é garantida). */ function criarValidadorArquivo({ input, nameEl, removeBtn, statusEl, tipo, getOperadoraKey }) { let estadoAtual = null; function limpar() { input.value = ""; nameEl.textContent = "Nenhum arquivo selecionado"; removeBtn.hidden = true; statusEl.hidden = true; statusEl.className = "ips-file-field__status"; statusEl.textContent = ""; estadoAtual = null; } async function validar() { const arquivo = input.files[0]; if (!arquivo) { limpar(); return; } nameEl.textContent = arquivo.name; removeBtn.hidden = false; statusEl.hidden = false; statusEl.className = "ips-file-field__status ips-file-field__status--pendente"; statusEl.textContent = "Validando arquivo..."; estadoAtual = null; try { const operadoraKey = getOperadoraKey ? getOperadoraKey() : null; const resultado = await pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey); estadoAtual = resultado; statusEl.className = `ips-file-field__status ${resultado.valido ? "ips-file-field__status--ok" : "ips-file-field__status--erro"}`; statusEl.textContent = `${resultado.valido ? "✓" : "✗"} ${resultado.mensagem}`; } catch (e) { estadoAtual = { valido: false, mensagem: e.message }; statusEl.className = "ips-file-field__status ips-file-field__status--erro"; statusEl.textContent = `✗ ${e.message}`; } } input.addEventListener("change", validar); removeBtn.addEventListener("click", limpar); return { limpar, revalidarSeAnexado: () => { if (input.files[0]) validar(); }, ehInvalido: () => estadoAtual !== null && estadoAtual.valido === false, }; } const validadorPlanilha = criarValidadorArquivo({ input: formPlanilha, nameEl: formPlanilhaName, removeBtn: formPlanilhaRemoveBtn, statusEl: formPlanilhaStatus, tipo: "planilha", }); const validadorArquivo = criarValidadorArquivo({ input: formArquivo, nameEl: formArquivoName, removeBtn: formArquivoRemoveBtn, statusEl: formArquivoStatus, tipo: "operadora", getOperadoraKey: () => formOperadora.value, }); function showView(view) { listView.hidden = view !== "list"; formView.hidden = view !== "form"; reviewView.hidden = view !== "review"; } function tiposLabel(tipos) { return (tipos || []).map((t) => PID_IPS_TIPO_LABELS[t] || t).join(" + "); } const IPS_LIST_PAGE_SIZE = 10; // Compara numericamente quando os dois valores parecem número (ex.: // "218" vs "9" — string pura ordenaria "9" depois de "218"), senão cai pra // comparação de texto (acentuada/case-insensitive) — cobre igual bem // "Cód. Empresa" (numérico) e as colunas de texto/data ISO (que já ordenam // certo por comparação de string). function comparaValoresLista(a, b) { const an = Number(a); const bn = Number(b); const ambosNumeros = a !== "" && b !== "" && !Number.isNaN(an) && !Number.isNaN(bn); if (ambosNumeros) return an - bn; return String(a).localeCompare(String(b), "pt-BR", { sensitivity: "base" }); } const PID_IPS_LISTA_SORT_VALOR = { codigo_empresa: (imp) => imp.codigo_empresa || "", nome_operadora: (imp) => imp.nome_operadora || "", tipos: (imp) => tiposLabel(imp.tipos_lancamento), status: (imp) => imp.status || "", criado_por: (imp) => imp.criado_por_nome || "", criado_em: (imp) => imp.criado_em || "", }; function renderListPagination(totalPaginas) { if (totalPaginas <= 1) { listPagination.innerHTML = ""; return; } listPagination.innerHTML = ` Página ${listPage} de ${totalPaginas} `; } // "Selecionar todos" só marca/desmarca os ids da página atualmente // visível (currentPageIds, recalculado a cada renderList) — nunca a // lista inteira nas outras páginas, para respeitar o que está na tela. // A seleção em si (selectedIds) persiste entre trocas de página/ordenação, // então dá pra combinar seleções de páginas diferentes antes de excluir // em lote; ela só é podada quando um id deixa de existir (refreshList). function updateSelectAllState() { if (!listSelectAll) return; const total = currentPageIds.length; const selecionadosNaPagina = currentPageIds.filter((id) => selectedIds.has(id)).length; listSelectAll.disabled = total === 0; listSelectAll.checked = total > 0 && selecionadosNaPagina === total; listSelectAll.indeterminate = selecionadosNaPagina > 0 && selecionadosNaPagina < total; } function updateBulkActionsUI() { const count = selectedIds.size; if (listBulkActions) listBulkActions.hidden = count === 0; if (listBulkCount) listBulkCount.textContent = `${count} selecionada${count === 1 ? "" : "s"}`; } function renderList() { listEmpty.hidden = importacoes.length !== 0; const extrator = PID_IPS_LISTA_SORT_VALOR[listSortKey]; const ordenadas = [...importacoes].sort((a, b) => comparaValoresLista(extrator(a), extrator(b)) * listSortDir); const totalPaginas = Math.max(1, Math.ceil(ordenadas.length / IPS_LIST_PAGE_SIZE)); listPage = Math.min(listPage, totalPaginas); const inicio = (listPage - 1) * IPS_LIST_PAGE_SIZE; const pagina = ordenadas.slice(inicio, inicio + IPS_LIST_PAGE_SIZE); currentPageIds = pagina.map((imp) => imp.id); listBody.innerHTML = ""; pagina.forEach((imp) => { const tr = document.createElement("tr"); tr.dataset.importacaoId = imp.id; tr.style.cursor = "pointer"; const criadoEm = new Date(imp.criado_em).toLocaleString("pt-BR"); const statusClasse = imp.status === "concluida" ? "status-pill--ativo" : "status-pill--inativo"; const statusTexto = imp.status === "concluida" ? "Concluída" : "Em revisão"; tr.innerHTML = ` ${imp.codigo_empresa || "—"} ${imp.nome_operadora} ${tiposLabel(imp.tipos_lancamento)} ${statusTexto} ${imp.criado_por_nome || "—"} ${criadoEm} `; listBody.appendChild(tr); }); listSortHeaders.forEach((th) => { const key = th.getAttribute("data-sort"); th.classList.toggle("is-sort-asc", key === listSortKey && listSortDir === 1); th.classList.toggle("is-sort-desc", key === listSortKey && listSortDir === -1); }); renderListPagination(totalPaginas); updateSelectAllState(); updateBulkActionsUI(); if (listCount) { if (ordenadas.length === 0) { listCount.textContent = ""; } else { const fim = Math.min(inicio + IPS_LIST_PAGE_SIZE, ordenadas.length); listCount.textContent = `Mostrando ${inicio + 1}–${fim} de ${ordenadas.length} registro${ordenadas.length === 1 ? "" : "s"}`; } } } async function refreshList() { try { importacoes = await pidFetchImportacoesPlanoSaude(); } catch (e) { importacoes = []; } const idsValidos = new Set(importacoes.map((imp) => imp.id)); Array.from(selectedIds).forEach((id) => { if (!idsValidos.has(id)) selectedIds.delete(id); }); renderList(); } function resetForm() { validadorPlanilha.limpar(); validadorArquivo.limpar(); formTipoMensalidade.checked = false; formTipoCoparticipacao.checked = false; custeioMensalidade.hidden = true; custeioCoparticipacao.hidden = true; custeioMensalidade.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false)); custeioCoparticipacao.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false)); PID_IPS_TIPOS.forEach((tipo) => { PID_IPS_PESSOAS.forEach((pessoa) => { const chave = `${tipo}-${pessoa}`; const regraBox = document.getElementById(`ips-regra-especifica-${chave}`); const limiteInput = document.getElementById(`ips-limite-${chave}`); const percentualInput = document.getElementById(`ips-percentual-${chave}`); if (regraBox) regraBox.hidden = true; if (limiteInput) limiteInput.value = ""; if (percentualInput) percentualInput.value = ""; }); }); regraAplicadaId = null; regraSelecionadaId = null; if (ipsRegraSearch) ipsRegraSearch.value = ""; fecharRegraComboList(); mostrarObservacoesRegra(null); formError.textContent = ""; } // "150,00" / 150 / 150.5 -> "150,00"/"150,50" — usado pra repopular os // inputs de limite/percentual (texto BR) a partir de uma regra salva, cujo // custeio_por_tipo já vem validado/parseado como float pelo backend. function formatarNumeroBr(valor) { if (valor === null || valor === undefined || valor === "") return ""; const numero = Number(valor); return Number.isNaN(numero) ? String(valor) : numero.toFixed(2).replace(".", ","); } /** * Lê o custeio atualmente preenchido no formulário (quais tipos de * lançamento estão marcados + o modo de custeio/limite/percentual de cada * combinação tipo×pessoa) — mesmo formato de tipos_lancamento/custeio_por_tipo * de RegraCusteioPlanoSaude, usado tanto pra montar o payload de "Salvar * regra atual..." quanto (via os mesmos campos) pro submit da importação. */ function coletarCusteioAtual() { const tipos_lancamento = []; if (formTipoMensalidade.checked) tipos_lancamento.push("mensalidade"); if (formTipoCoparticipacao.checked) tipos_lancamento.push("coparticipacao"); const custeio_por_tipo = {}; PID_IPS_TIPOS.forEach((tipo) => { custeio_por_tipo[tipo] = {}; PID_IPS_PESSOAS.forEach((pessoa) => { const chave = `${tipo}-${pessoa}`; const escolhido = document.querySelector(`input[name="ips-custeio-${chave}"]:checked`); if (!escolhido) return; const entrada = { modo: escolhido.value }; if (escolhido.value === "especifica") { entrada.limite_valor = document.getElementById(`ips-limite-${chave}`).value.trim(); entrada.percentual = document.getElementById(`ips-percentual-${chave}`).value.trim(); } custeio_por_tipo[tipo][pessoa] = entrada; }); }); return { tipos_lancamento, custeio_por_tipo }; } // Valida se o custeio dos tipos marcados está completo (titular e // dependente de cada um) — reaproveitado pelo submit da importação e por // "Salvar regra atual...", já que as duas ações exigem a mesma coisa: // um custeio_por_tipo completo pros tipos selecionados. function mensagemErroCusteio(tipos) { if (!tipos.length) return "Selecione ao menos um tipo de importação."; for (const tipo of tipos) { for (const pessoa of PID_IPS_PESSOAS) { const chave = `${tipo}-${pessoa}`; const rotulo = `${PID_IPS_TIPO_LABELS[tipo]} (${PID_IPS_PESSOA_LABELS[pessoa]})`; const escolhido = document.querySelector(`input[name="ips-custeio-${chave}"]:checked`); if (!escolhido) return `Informe o custeio para ${rotulo}.`; if (escolhido.value === "especifica") { const limite = document.getElementById(`ips-limite-${chave}`).value.trim(); const percentual = document.getElementById(`ips-percentual-${chave}`).value.trim(); if (!limite && !percentual) return `Informe o limite de valor e/ou o percentual de custeio para ${rotulo}.`; } } } return null; } // Repõe no formulário uma regra salva (RegraCusteioPlanoSaude) — os campos // continuam 100% editáveis depois, é só um preenchimento em massa, não um // modo "somente leitura". `regra.custeio_por_tipo` já vem validado do // backend (limite_valor/percentual como float, não texto BR). function aplicarCusteio(regra) { const tipos = Array.isArray(regra.tipos_lancamento) ? regra.tipos_lancamento : []; formTipoMensalidade.checked = tipos.includes("mensalidade"); custeioMensalidade.hidden = !formTipoMensalidade.checked; formTipoCoparticipacao.checked = tipos.includes("coparticipacao"); custeioCoparticipacao.hidden = !formTipoCoparticipacao.checked; const custeioPorTipo = regra.custeio_por_tipo || {}; PID_IPS_TIPOS.forEach((tipo) => { PID_IPS_PESSOAS.forEach((pessoa) => { const chave = `${tipo}-${pessoa}`; const regraBox = document.getElementById(`ips-regra-especifica-${chave}`); const limiteInput = document.getElementById(`ips-limite-${chave}`); const percentualInput = document.getElementById(`ips-percentual-${chave}`); document.querySelectorAll(`input[name="ips-custeio-${chave}"]`).forEach((r) => (r.checked = false)); if (limiteInput) limiteInput.value = ""; if (percentualInput) percentualInput.value = ""; if (regraBox) regraBox.hidden = true; const entrada = custeioPorTipo[tipo] && custeioPorTipo[tipo][pessoa]; if (!entrada || !entrada.modo) return; const radio = document.querySelector(`input[name="ips-custeio-${chave}"][value="${entrada.modo}"]`); if (radio) radio.checked = true; if (entrada.modo === "especifica") { if (limiteInput) limiteInput.value = formatarNumeroBr(entrada.limite_valor); if (percentualInput) percentualInput.value = formatarNumeroBr(entrada.percentual); if (regraBox) regraBox.hidden = false; } }); }); } function operadoraLabel(key) { const operadora = operadorasCache.find((op) => op.key === key); return operadora ? operadora.label : key; } function mostrarObservacoesRegra(regra) { if (regra && regra.observacoes) { ipsRegraObservacoes.textContent = regra.observacoes; ipsRegraObservacoes.hidden = false; } else if (ipsRegraObservacoes) { ipsRegraObservacoes.hidden = true; ipsRegraObservacoes.textContent = ""; } } // Aplica a regra no formulário (custeio + operadora, se ela ainda existir // no // simples (deixou de escalar visualmente com o crescimento do banco de // regras salvas). function fecharRegraComboList() { if (ipsRegraComboList) { ipsRegraComboList.hidden = true; ipsRegraComboList.innerHTML = ""; } } function renderRegraComboList(filtro) { if (!ipsRegraComboList) return; const termo = (filtro || "").trim().toLowerCase(); const filtradas = termo ? regras.filter((r) => r.nome.toLowerCase().includes(termo)) : regras; if (!filtradas.length) { ipsRegraComboList.innerHTML = `

Nenhuma regra encontrada.

`; } else { ipsRegraComboList.innerHTML = filtradas .map((r) => `
${escapeHtml(r.nome)}
`) .join(""); } ipsRegraComboList.hidden = false; } async function refreshRegras() { try { regras = await pidFetchRegrasCusteioPlanoSaude(); } catch (e) { regras = []; } // Se a regra selecionada no combobox foi excluída em outro lugar // enquanto isso (ex.: pelo modal "Ver regras salvas"), limpa a seleção // em vez de deixar o campo de busca mostrando um nome que não existe mais. if (regraSelecionadaId !== null && !regras.some((r) => r.id === regraSelecionadaId)) { regraSelecionadaId = null; if (ipsRegraSearch) ipsRegraSearch.value = ""; } } function renderRegrasLista() { if (!regrasLista) return; if (!regras.length) { regrasLista.innerHTML = `

Nenhuma regra salva ainda.

`; return; } regrasLista.innerHTML = regras .map((r) => { const meta = `${r.operadora ? operadoraLabel(r.operadora) : "Qualquer operadora"} · ${tiposLabel(r.tipos_lancamento)}`; return `

${escapeHtml(r.nome)}

${escapeHtml(meta)}

${r.observacoes ? `

${escapeHtml(r.observacoes)}

` : ""}
`; }) .join(""); } function abrirRegraSaveModal() { regraSaveError.textContent = ""; const regraBase = regras.find((r) => r.id === regraAplicadaId) || null; regraSaveNome.value = regraBase ? regraBase.nome : ""; regraSaveObservacoes.value = regraBase ? regraBase.observacoes || "" : ""; if (regraBase) { regraSaveAtualizarField.hidden = false; regraSaveAtualizar.checked = true; regraSaveAtualizarLabel.textContent = `Atualizar a regra selecionada ("${regraBase.nome}")`; } else { regraSaveAtualizarField.hidden = true; regraSaveAtualizar.checked = false; } regraSaveTitle.textContent = regraBase ? "Salvar regra de custeio" : "Salvar nova regra de custeio"; regraSaveModal.hidden = false; regraSaveNome.focus(); } async function openNewForm() { resetForm(); try { operadorasCache = await pidFetchOperadorasPlanoSaude(); formOperadora.innerHTML = `` + operadorasCache.map((op) => ``).join(""); } catch (e) { operadorasCache = []; formOperadora.innerHTML = ""; } await refreshRegras(); showView("form"); } function abrirRevisao(importacao) { importacaoAtual = importacao; activeTab = null; reviewSubtitle.textContent = `${importacao.nome_operadora} — criada em ${new Date(importacao.criado_em).toLocaleString("pt-BR")}`; reviewError.textContent = ""; renderTabs(); showView("review"); } function renderTabs() { const tipos = importacaoAtual.tipos_lancamento || []; const tabKeys = [...tipos, "auditoria"]; if (!activeTab || !tabKeys.includes(activeTab)) activeTab = tabKeys[0]; tabsEl.innerHTML = tabKeys .map((key) => { const label = key === "auditoria" ? "Auditoria" : PID_IPS_TIPO_LABELS[key] || key; return ``; }) .join(""); renderPanels(); } function escapeAttr(value) { return (value === null || value === undefined ? "" : String(value)).replace(/"/g, """); } // Igual a escapeAttr, mas também escapa "<"/">"/"&" — usado pra inserir via // innerHTML texto livre digitado pelo colaborador (nome/observações de uma // regra de custeio salva), que ao contrário do resto dos campos desta tela // (nomes de operadora/arquivo, vindos de fontes fixas) pode conter // qualquer caractere. function escapeHtml(value) { return (value === null || value === undefined ? "" : String(value)).replace( /[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch] ); } function colGroupHtml(campos, widths, extraColWidth) { const extraCol = extraColWidth ? `` : ""; const cols = campos.map(([campo, , largura]) => ``).join(""); return `${extraCol}${cols}`; } function resizeHandleHtml(campo, target) { return ``; } // Colunas têm ordem fixa (não são mais arrastáveis) — só o clique pra // ordenar as linhas por essa coluna continua disponível. function colHeaderHtml(campo, label, target, sortState) { const ordenadaPorMim = sortState && sortState.key === campo; const sortClasse = ordenadaPorMim ? (sortState.dir === 1 ? " is-sort-asc" : " is-sort-desc") : ""; return `${label} ↕${resizeHandleHtml(campo, target)}`; } function wireColumnResize(root) { root.querySelectorAll(".ips-col-resize-handle").forEach((handle) => { handle.addEventListener("mousedown", (event) => { event.preventDefault(); const th = handle.closest("th"); const table = th.closest("table"); const colIndex = Array.from(th.parentElement.children).indexOf(th); const col = table.querySelectorAll("col")[colIndex]; if (!col) return; const campo = handle.getAttribute("data-resize-campo"); const widthsState = handle.getAttribute("data-resize-target") === "auditoria" ? colWidthsAuditoria : colWidthsLinha; const startX = event.clientX; const startWidth = th.getBoundingClientRect().width; document.body.classList.add("is-resizing-column"); function onMouseMove(e) { const novaLargura = Math.max(PID_IPS_COL_MIN_WIDTH, Math.round(startWidth + (e.clientX - startX))); col.style.width = `${novaLargura}px`; } function onMouseUp() { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); document.body.classList.remove("is-resizing-column"); widthsState[campo] = parseInt(col.style.width, 10); } document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }); }); } function panelHtmlParaTipo(tipo) { const campos = PID_IPS_CAMPOS_LINHA; let linhas = (importacaoAtual.linhas || []).filter((l) => l.tipo_lancamento === tipo); if (linhaSortKey) { linhas = [...linhas].sort((a, b) => comparaValorRevisao(a[linhaSortKey], b[linhaSortKey]) * linhaSortDir); } const resumo = (importacaoAtual.resumo_por_tipo || []).find((r) => r.tipo_lancamento === tipo); const resumoHtml = resumo ? `
${resumo.total_linhas} linhas no total ${resumo.total_lancadas} com valor lançado ${resumo.total_auditoria} em auditoria
` : ""; const linhasHtml = linhas .map( (linha) => ` ${campos .map( ([campo]) => `` ) .join("")} ` ) .join(""); return ` ${resumoHtml}
${colGroupHtml(campos, colWidthsLinha, 44)} ${campos .map(([campo, label]) => colHeaderHtml(campo, label, "linha", { key: linhaSortKey, dir: linhaSortDir })) .join("")}${linhasHtml || ``}
Nenhuma linha.
`; } function panelHtmlAuditoria() { const campos = PID_IPS_CAMPOS_AUDITORIA; let itens = importacaoAtual.itens_auditoria || []; if (!itens.length) { return `

Nenhum item em auditoria — tudo foi lançado automaticamente.

`; } if (auditoriaSortKey) { itens = [...itens].sort((a, b) => comparaValorRevisao(a[auditoriaSortKey], b[auditoriaSortKey]) * auditoriaSortDir); } const linhasHtml = itens .map((item) => { let acaoHtml = `—`; if (item.resolvida) { acaoHtml = `Resolvido${ item.linha_vinculada_nome ? ` — ${item.linha_vinculada_nome}` : "" }`; } else if (PID_IPS_MOTIVOS_RESOLVIVEIS.includes(item.motivo)) { acaoHtml = ``; } return ` ${acaoHtml} ${item.nome || "—"} ${item.cpf || "—"} ${item.tipo === "T" ? "Titular" : "Dependente"} ${PID_IPS_TIPO_LABELS[item.tipo_lancamento] || item.tipo_lancamento || "—"} ${Number(item.valor).toFixed(2).replace(".", ",")} ${PID_IPS_MOTIVO_LABELS[item.motivo] || item.motivo} ${item.detalhe || ""} `; }) .join(""); return `
${colGroupHtml(campos, colWidthsAuditoria, PID_IPS_AUDITORIA_ACAO_WIDTH)} ${campos .map(([campo, label]) => colHeaderHtml(campo, label, "auditoria", { key: auditoriaSortKey, dir: auditoriaSortDir })) .join("")}${linhasHtml}
Ação
`; } function renderPanels() { const tipos = importacaoAtual.tipos_lancamento || []; panelsEl.innerHTML = [...tipos, "auditoria"] .map((key) => { const html = key === "auditoria" ? panelHtmlAuditoria() : panelHtmlParaTipo(key); return `
${html}
`; }) .join(""); wireColumnResize(panelsEl); } // ------------------------------------------------------------------ // Vincular pessoa (resolução manual de NOME_DIVERGENTE/NAO_CADASTRADO) // ------------------------------------------------------------------ function linhaEhTitular(linha) { return !(linha.nome_dependente || "").trim() && !(linha.cpf_dependente || "").trim(); } function candidatosVincular(item) { const ehTitular = item.tipo === "T"; return (importacaoAtual.linhas || []).filter( (l) => l.tipo_lancamento === item.tipo_lancamento && l.valor === "0" && l.valor_empresa === "0" && linhaEhTitular(l) === ehTitular ); } function renderVincularLista(filtro) { const termo = (filtro || "").trim().toLowerCase(); const candidatos = candidatosVincular(vincularItemAtual).filter((l) => { const nomePrincipal = (l.nome_dependente || l.nome_func || "").toLowerCase(); const nomeTitular = (l.nome_func || "").toLowerCase(); return nomePrincipal.includes(termo) || nomeTitular.includes(termo); }); if (!candidatos.length) { vincularLista.innerHTML = `

Nenhuma linha em branco encontrada na planilha padrão.

`; return; } vincularLista.innerHTML = candidatos .map((l) => { const nomePrincipal = l.nome_dependente || l.nome_func; const contexto = l.nome_dependente ? `Titular: ${l.nome_func}` : "Titular"; return ` `; }) .join(""); } function abrirVincularModal(item) { vincularItemAtual = item; vincularLinhaSelecionada = null; vincularError.textContent = ""; vincularConfirmBtn.disabled = true; vincularSearch.value = ""; vincularResumo.textContent = `"${item.nome}" (${item.tipo === "T" ? "Titular" : "Dependente"} — ${ PID_IPS_TIPO_LABELS[item.tipo_lancamento] || item.tipo_lancamento }) não foi encontrado automaticamente. Selecione a pessoa correta na planilha padrão:`; renderVincularLista(""); vincularModal.hidden = false; } function fecharVincularModal() { vincularModal.hidden = true; vincularItemAtual = null; vincularLinhaSelecionada = null; } if (vincularModal) { vincularModal.addEventListener("click", (event) => { if (event.target === vincularModal) fecharVincularModal(); }); } if (vincularCancelBtn) vincularCancelBtn.addEventListener("click", fecharVincularModal); if (vincularSearch) { vincularSearch.addEventListener("input", () => renderVincularLista(vincularSearch.value)); } if (vincularLista) { vincularLista.addEventListener("change", (event) => { if (event.target.name !== "ips-vincular-linha") return; vincularLinhaSelecionada = Number(event.target.value); vincularConfirmBtn.disabled = false; }); } if (vincularConfirmBtn) { vincularConfirmBtn.addEventListener("click", async () => { if (!vincularItemAtual || !vincularLinhaSelecionada) return; vincularError.textContent = ""; vincularConfirmBtn.disabled = true; try { await pidResolverAuditoriaPlanoSaude(vincularItemAtual.id, vincularLinhaSelecionada); importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id); fecharVincularModal(); renderTabs(); } catch (e) { vincularError.textContent = e.message; vincularConfirmBtn.disabled = false; } }); } if (newBtn) newBtn.addEventListener("click", openNewForm); if (formCancelBtn) formCancelBtn.addEventListener("click", () => showView("list")); if (formBackBtn) formBackBtn.addEventListener("click", () => showView("list")); if (reviewBackBtn) { reviewBackBtn.addEventListener("click", async () => { showView("list"); await refreshList(); }); } if (formOperadora) { formOperadora.addEventListener("change", () => validadorArquivo.revalidarSeAnexado()); } if (formTipoMensalidade) { formTipoMensalidade.addEventListener("change", () => { custeioMensalidade.hidden = !formTipoMensalidade.checked; }); } if (formTipoCoparticipacao) { formTipoCoparticipacao.addEventListener("change", () => { custeioCoparticipacao.hidden = !formTipoCoparticipacao.checked; }); } if (ipsRegraSearch) { ipsRegraSearch.addEventListener("focus", () => renderRegraComboList(ipsRegraSearch.value)); ipsRegraSearch.addEventListener("input", () => { // Digitar de novo invalida a seleção anterior — só volta a valer // depois de clicar numa regra da lista (evita "Aplicar" usar uma // regra antiga enquanto o texto mostrado já é outra busca). regraSelecionadaId = null; renderRegraComboList(ipsRegraSearch.value); }); } if (ipsRegraComboList) { ipsRegraComboList.addEventListener("click", (event) => { const item = event.target.closest("[data-regra-id]"); if (!item) return; const id = Number(item.getAttribute("data-regra-id")); const regra = regras.find((r) => r.id === id); if (!regra) return; regraSelecionadaId = regra.id; ipsRegraSearch.value = regra.nome; fecharRegraComboList(); mostrarObservacoesRegra(regra); }); } document.addEventListener("click", (event) => { if (ipsRegraCombo && !ipsRegraCombo.contains(event.target)) fecharRegraComboList(); }); if (ipsRegraAplicarBtn) { ipsRegraAplicarBtn.addEventListener("click", () => { if (!regraSelecionadaId) { formError.textContent = "Selecione uma regra salva pra aplicar."; return; } const regra = regras.find((r) => r.id === regraSelecionadaId); if (regra) aplicarRegraNoFormulario(regra); }); } if (ipsRegraSalvarBtn) { ipsRegraSalvarBtn.addEventListener("click", () => { const { tipos_lancamento } = coletarCusteioAtual(); const erro = mensagemErroCusteio(tipos_lancamento); if (erro) { formError.textContent = erro; return; } formError.textContent = ""; abrirRegraSaveModal(); }); } if (regraSaveCancelBtn) regraSaveCancelBtn.addEventListener("click", () => (regraSaveModal.hidden = true)); if (regraSaveModal) { regraSaveModal.addEventListener("click", (event) => { if (event.target === regraSaveModal) regraSaveModal.hidden = true; }); } if (regraSaveConfirmBtn) { regraSaveConfirmBtn.addEventListener("click", async () => { const nome = regraSaveNome.value.trim(); if (!nome) { regraSaveError.textContent = "Informe um nome para a regra."; return; } const { tipos_lancamento, custeio_por_tipo } = coletarCusteioAtual(); const payload = { nome, operadora: formOperadora.value || "", tipos_lancamento, custeio_por_tipo, observacoes: regraSaveObservacoes.value.trim(), }; const atualizando = !regraSaveAtualizarField.hidden && regraSaveAtualizar.checked && regraAplicadaId; regraSaveConfirmBtn.disabled = true; try { const regraSalva = atualizando ? await pidAtualizarRegraCusteioPlanoSaude(regraAplicadaId, payload) : await pidCriarRegraCusteioPlanoSaude(payload); await refreshRegras(); regraAplicadaId = regraSalva.id; regraSelecionadaId = regraSalva.id; if (ipsRegraSearch) ipsRegraSearch.value = regraSalva.nome; mostrarObservacoesRegra(regraSalva); regraSaveModal.hidden = true; } catch (e) { regraSaveError.textContent = e.message; } finally { regraSaveConfirmBtn.disabled = false; } }); } if (regrasGerenciarBtn) { regrasGerenciarBtn.addEventListener("click", async () => { regrasError.textContent = ""; await refreshRegras(); renderRegrasLista(); regrasModal.hidden = false; }); } if (regrasFecharBtn) regrasFecharBtn.addEventListener("click", () => (regrasModal.hidden = true)); if (regrasModal) { regrasModal.addEventListener("click", (event) => { if (event.target === regrasModal) regrasModal.hidden = true; }); } if (regrasLista) { regrasLista.addEventListener("click", async (event) => { const aplicarBtn = event.target.closest("[data-regra-aplicar]"); if (aplicarBtn) { const id = Number(aplicarBtn.getAttribute("data-regra-aplicar")); const regra = regras.find((r) => r.id === id); if (regra) { aplicarRegraNoFormulario(regra); regrasModal.hidden = true; } return; } const excluirBtn = event.target.closest("[data-regra-excluir]"); if (excluirBtn) { const id = Number(excluirBtn.getAttribute("data-regra-excluir")); const regra = regras.find((r) => r.id === id); if (!regra) return; if (!window.confirm(`Excluir a regra "${regra.nome}"? Essa ação não pode ser desfeita.`)) return; excluirBtn.disabled = true; try { await pidExcluirRegraCusteioPlanoSaude(id); if (regraAplicadaId === id) regraAplicadaId = null; await refreshRegras(); renderRegrasLista(); regrasError.textContent = ""; } catch (e) { regrasError.textContent = e.message; excluirBtn.disabled = false; } } }); } PID_IPS_TIPOS.forEach((tipo) => { const custeioBox = document.getElementById(`ips-custeio-${tipo}`); if (!custeioBox) return; custeioBox.addEventListener("change", (event) => { const pessoa = PID_IPS_PESSOAS.find((p) => event.target.name === `ips-custeio-${tipo}-${p}`); if (!pessoa) return; const regraBox = document.getElementById(`ips-regra-especifica-${tipo}-${pessoa}`); if (regraBox) regraBox.hidden = event.target.value !== "especifica"; }); }); if (tabsEl) { tabsEl.addEventListener("click", (event) => { const btn = event.target.closest("[data-ips-tab]"); if (!btn) return; activeTab = btn.getAttribute("data-ips-tab"); renderTabs(); }); } if (panelsEl) { panelsEl.addEventListener("change", async (event) => { const input = event.target.closest("[data-linha-campo]"); if (!input) return; const row = input.closest("[data-linha-id]"); const linhaId = Number(row.getAttribute("data-linha-id")); const campo = input.getAttribute("data-linha-campo"); try { await pidAtualizarLinhaPlanoSaude(linhaId, { [campo]: input.value.trim() }); reviewError.textContent = ""; } catch (e) { reviewError.textContent = e.message; } }); panelsEl.addEventListener("click", async (event) => { if (!event.target.closest(".ips-col-resize-handle")) { const sortTh = event.target.closest("[data-col-campo]"); if (sortTh) { const campo = sortTh.getAttribute("data-col-campo"); if (sortTh.getAttribute("data-col-target") === "auditoria") { if (auditoriaSortKey === campo) auditoriaSortDir *= -1; else { auditoriaSortKey = campo; auditoriaSortDir = 1; } } else { if (linhaSortKey === campo) linhaSortDir *= -1; else { linhaSortKey = campo; linhaSortDir = 1; } } renderPanels(); return; } } const addBtn = event.target.closest("[data-ips-add-linha]"); if (addBtn) { const tipo = addBtn.getAttribute("data-ips-add-linha"); addBtn.disabled = true; try { const novaLinha = await pidCriarLinhaPlanoSaude(importacaoAtual.id, tipo); importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id); reviewError.textContent = ""; renderTabs(); const novaRow = panelsEl.querySelector(`[data-linha-id="${novaLinha.id}"]`); const primeiroInput = novaRow && novaRow.querySelector("[data-linha-campo]"); if (primeiroInput) { primeiroInput.scrollIntoView({ block: "nearest" }); primeiroInput.focus(); } } catch (e) { reviewError.textContent = e.message; addBtn.disabled = false; } return; } const removeBtn = event.target.closest("[data-remover-linha]"); if (removeBtn) { const linhaId = Number(removeBtn.getAttribute("data-remover-linha")); if (!window.confirm("Remover esta linha? Ela não vai aparecer no arquivo gerado.")) return; removeBtn.disabled = true; try { await pidExcluirLinhaPlanoSaude(linhaId); importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id); reviewError.textContent = ""; renderTabs(); } catch (e) { reviewError.textContent = e.message; removeBtn.disabled = false; } return; } const vincularBtn = event.target.closest("[data-vincular-auditoria]"); if (vincularBtn) { const itemId = Number(vincularBtn.getAttribute("data-vincular-auditoria")); const item = (importacaoAtual.itens_auditoria || []).find((i) => i.id === itemId); if (item) abrirVincularModal(item); } }); } if (gerarBtn) { gerarBtn.addEventListener("click", async () => { reviewError.textContent = ""; gerarBtn.disabled = true; try { await pidGerarArquivoPlanoSaude(importacaoAtual.id); } catch (e) { reviewError.textContent = e.message; } finally { gerarBtn.disabled = false; } }); } if (listBody) { listBody.addEventListener("click", async (event) => { const removeBtn = event.target.closest("[data-remover-importacao]"); if (removeBtn) { const id = Number(removeBtn.getAttribute("data-remover-importacao")); if (!window.confirm("Excluir esta importação do histórico? Essa ação não pode ser desfeita.")) return; try { await pidExcluirImportacaoPlanoSaude(id); selectedIds.delete(id); await refreshList(); } catch (e) { window.alert(e.message); } return; } if (event.target.closest("[data-select-id]")) return; const row = event.target.closest("[data-importacao-id]"); if (!row) return; const id = Number(row.getAttribute("data-importacao-id")); try { const detalhe = await pidFetchImportacaoPlanoSaude(id); abrirRevisao(detalhe); } catch (e) { window.alert(e.message); } }); listBody.addEventListener("change", (event) => { const checkbox = event.target.closest("[data-select-id]"); if (!checkbox) return; const id = Number(checkbox.getAttribute("data-select-id")); if (checkbox.checked) selectedIds.add(id); else selectedIds.delete(id); updateSelectAllState(); updateBulkActionsUI(); }); } if (listSelectAll) { listSelectAll.addEventListener("change", () => { if (listSelectAll.checked) { currentPageIds.forEach((id) => selectedIds.add(id)); } else { currentPageIds.forEach((id) => selectedIds.delete(id)); } renderList(); }); } if (listBulkDeleteBtn) { listBulkDeleteBtn.addEventListener("click", async () => { const ids = Array.from(selectedIds); if (!ids.length) return; const confirmMsg = ids.length === 1 ? "Excluir esta importação do histórico? Essa ação não pode ser desfeita." : `Excluir estas ${ids.length} importações do histórico? Essa ação não pode ser desfeita.`; if (!window.confirm(confirmMsg)) return; listBulkDeleteBtn.disabled = true; try { await Promise.all(ids.map((id) => pidExcluirImportacaoPlanoSaude(id))); selectedIds.clear(); } catch (e) { window.alert(e.message); } finally { listBulkDeleteBtn.disabled = false; await refreshList(); } }); } listSortHeaders.forEach((th) => { th.addEventListener("click", () => { const key = th.getAttribute("data-sort"); if (listSortKey === key) { listSortDir *= -1; } else { listSortKey = key; listSortDir = 1; } listPage = 1; renderList(); }); }); if (listPagination) { listPagination.addEventListener("click", (event) => { if (event.target.closest("#ips-list-prev-btn")) { listPage -= 1; renderList(); } else if (event.target.closest("#ips-list-next-btn")) { listPage += 1; renderList(); } }); } if (formSubmitBtn) { formSubmitBtn.addEventListener("click", async () => { const { tipos_lancamento: tipos, custeio_por_tipo: custeio } = coletarCusteioAtual(); if (!formOperadora.value) { formError.textContent = "Selecione a operadora."; return; } if (!formPlanilha.files[0]) { formError.textContent = "Anexe a planilha padrão do Questor."; return; } if (!formArquivo.files[0]) { formError.textContent = "Anexe o arquivo da operadora."; return; } if (validadorPlanilha.ehInvalido() || validadorArquivo.ehInvalido()) { formError.textContent = "Corrija os arquivos anexados (veja a mensagem abaixo de cada um) antes de processar."; return; } const erroCusteio = mensagemErroCusteio(tipos); if (erroCusteio) { formError.textContent = erroCusteio; return; } formError.textContent = ""; const formData = new FormData(); formData.append("operadora", formOperadora.value); formData.append("planilha_padrao", formPlanilha.files[0]); formData.append("arquivo_operadora", formArquivo.files[0]); formData.append("tipos_lancamento", tipos.join(",")); tipos.forEach((tipo) => { PID_IPS_PESSOAS.forEach((pessoa) => { const entrada = custeio[tipo][pessoa]; formData.append(`custeio_${tipo}_${pessoa}`, entrada.modo); if (entrada.modo === "especifica") { formData.append(`limite_valor_${tipo}_${pessoa}`, entrada.limite_valor || ""); formData.append(`percentual_${tipo}_${pessoa}`, entrada.percentual || ""); } }); }); formSubmitBtn.disabled = true; formSubmitBtn.textContent = "Processando..."; try { const importacao = await pidCriarImportacaoPlanoSaude(formData); abrirRevisao(importacao); await refreshList(); } catch (e) { formError.textContent = e.message; } finally { formSubmitBtn.disabled = false; formSubmitBtn.textContent = "Processar"; } }); } await refreshList(); });