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], ]; // Lookup campo -> label pra aba "Alterações" (ex.: "valor" -> "Valor"), // derivado de PID_IPS_CAMPOS_LINHA pra nunca divergir do rótulo já usado na // própria coluna da tabela de Mensalidade/Coparticipação. const PID_IPS_CAMPO_LABELS = Object.fromEntries(PID_IPS_CAMPOS_LINHA.map(([campo, label]) => [campo, label])); // Espelha ImportacaoPlanoSaudeAlteracao.TIPO_CHOICES (models.py). const PID_IPS_ALTERACAO_TIPO_LABELS = { edicao: "Edição", inclusao: "Inclusão", exclusao: "Exclusão", }; 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 pidFetchRegrasEmpresaPlanoSaude() { return (await pidApiRequest("/importacoes-plano-saude/regras-empresa/")) || []; } 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 pidReabrirImportacaoPlanoSaude(id) { return pidApiRequest(`/importacoes-plano-saude/${id}/reabrir/`, { method: "POST" }); } 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 pidReverterAlteracaoPlanoSaude(id) { return pidApiRequest(`/importacoes-plano-saude-alteracoes/${id}/reverter/`, { method: "POST" }); } 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" }); } async function pidBuscarNomeEmpresaPlanoSaude(codigoEmpresa) { return pidApiRequest(`/regras-custeio-plano-saude/nome-empresa/?codigo_empresa=${encodeURIComponent(codigoEmpresa)}`); } /** * 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"); // Origem da planilha padrão: buscar do Questor (padrão) ou anexar // manualmente (alternativa — Questor fora do ar, ou empresa ainda não // migrada). Ver ImportacaoPlanoSaudeCreateSerializer.validate() no // backend, que exige exatamente uma das duas. const planilhaOrigemRadios = Array.from(document.querySelectorAll('input[name="ips-planilha-origem"]')); const planilhaQuestorField = document.getElementById("ips-planilha-questor-field"); const planilhaUploadField = document.getElementById("ips-planilha-upload-field"); const formCompetencia = document.getElementById("ips-form-competencia"); 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 formTipoRegraEmpresa = document.getElementById("ips-form-tipo-regra-empresa"); const custeioMensalidade = document.getElementById("ips-custeio-mensalidade"); const custeioCoparticipacao = document.getElementById("ips-custeio-coparticipacao"); const regraEmpresaBox = document.getElementById("ips-regra-empresa-box"); const regraEmpresaAtual = document.getElementById("ips-regra-empresa-atual"); const regraEmpresaSelecionarBtn = document.getElementById("ips-regra-empresa-selecionar-btn"); const regraEmpresaModal = document.getElementById("ips-regra-empresa-modal"); const regraEmpresaLista = document.getElementById("ips-regra-empresa-lista"); const regraEmpresaError = document.getElementById("ips-regra-empresa-error"); const regraEmpresaCancelBtn = document.getElementById("ips-regra-empresa-cancel-btn"); // Tela "Nova Importação" — Empresa/Operadora só resolvem uma regra já // cadastrada (ver "Cadastro de Regras" abaixo), nunca a editam. const impEmpresaSearch = document.getElementById("ips-imp-empresa-search"); const impEmpresaList = document.getElementById("ips-imp-empresa-combo-list"); const impEmpresaValor = document.getElementById("ips-imp-empresa-valor"); const impEmpresaLimparBtn = document.getElementById("ips-imp-empresa-limpar-btn"); const impEmpresaToggleBtn = document.getElementById("ips-imp-empresa-toggle-btn"); const impOperadoraSearch = document.getElementById("ips-imp-operadora-search"); const impOperadoraList = document.getElementById("ips-imp-operadora-combo-list"); const impOperadoraLimparBtn = document.getElementById("ips-imp-operadora-limpar-btn"); const impOperadoraToggleBtn = document.getElementById("ips-imp-operadora-toggle-btn"); const impResumo = document.getElementById("ips-imp-resumo"); const impResumoTipos = document.getElementById("ips-imp-resumo-tipos"); const impResumoMensalidade = document.getElementById("ips-imp-resumo-mensalidade"); const impResumoCoparticipacao = document.getElementById("ips-imp-resumo-coparticipacao"); const impResumoObservacoes = document.getElementById("ips-imp-resumo-observacoes"); // Modal "Cadastro de Regras" — cadastro/edição de RegraCusteioPlanoSaude, // separado da execução da importação (ver acima). const regracadBtn = document.getElementById("ips-regracad-btn"); const regracadModal = document.getElementById("ips-regracad-modal"); const regracadToolbar = document.getElementById("ips-regracad-toolbar"); const regracadEmpresaSearch = document.getElementById("ips-regracad-empresa-search"); const regracadEmpresaList = document.getElementById("ips-regracad-empresa-combo-list"); const regracadEmpresaLimparBtn = document.getElementById("ips-regracad-empresa-limpar-btn"); const regracadEmpresaToggleBtn = document.getElementById("ips-regracad-empresa-toggle-btn"); const regracadOperadoraSearch = document.getElementById("ips-regracad-operadora-search"); const regracadOperadoraList = document.getElementById("ips-regracad-operadora-combo-list"); const regracadOperadoraLimparBtn = document.getElementById("ips-regracad-operadora-limpar-btn"); const regracadOperadoraToggleBtn = document.getElementById("ips-regracad-operadora-toggle-btn"); const regracadNovaBtn = document.getElementById("ips-regracad-nova-btn"); const regracadNovaOperadoraBtn = document.getElementById("ips-regracad-nova-operadora-btn"); const regracadVazio = document.getElementById("ips-regracad-vazio"); const regracadModo = document.getElementById("ips-regracad-modo"); const regracadNovoView = document.getElementById("ips-regracad-novo-view"); const regracadNovoCodigoEmpresa = document.getElementById("ips-regracad-novo-codigo-empresa"); const regracadNovoEmpresaNome = document.getElementById("ips-regracad-novo-empresa-nome"); const regracadNovoOperadoraSearch = document.getElementById("ips-regracad-novo-operadora-search"); const regracadNovoOperadoraList = document.getElementById("ips-regracad-novo-operadora-combo-list"); const regracadNovoOperadoraToggleBtn = document.getElementById("ips-regracad-novo-operadora-toggle-btn"); const regracadNovoOperadoraDuplicada = document.getElementById("ips-regracad-novo-operadora-duplicada"); const regracadFormView = document.getElementById("ips-regracad-form-view"); const regracadObservacoes = document.getElementById("ips-regracad-observacoes"); const regracadError = document.getElementById("ips-regracad-error"); const regracadFecharBtn = document.getElementById("ips-regracad-fechar-btn"); const regracadNovoCancelarBtn = document.getElementById("ips-regracad-novo-cancelar-btn"); const regracadExcluirBtn = document.getElementById("ips-regracad-excluir-btn"); const regracadSalvarBtn = document.getElementById("ips-regracad-salvar-btn"); const regracadCriarBtn = document.getElementById("ips-regracad-criar-btn"); const regracadConfirmFecharModal = document.getElementById("ips-regracad-confirm-fechar-modal"); const regracadConfirmFecharCancelarBtn = document.getElementById("ips-regracad-confirm-fechar-cancelar-btn"); const regracadConfirmFecharOkBtn = document.getElementById("ips-regracad-confirm-fechar-ok-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 reviewEmpresa = document.getElementById("ips-review-empresa"); const reviewSubtitle = document.getElementById("ips-review-subtitle"); const reviewRegraEmpresaObs = document.getElementById("ips-review-regra-empresa-obs"); const reviewRegraEmpresaObsLabel = document.getElementById("ips-review-regra-empresa-obs-label"); const reviewRegraEmpresaObsTexto = document.getElementById("ips-review-regra-empresa-obs-texto"); const reviewError = document.getElementById("ips-review-error"); const reviewBackBtn = document.getElementById("ips-review-back-btn"); const reviewEditarBtn = document.getElementById("ips-review-editar-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; // Filtros da lista principal, estilo Excel — client-side, sobre o array // já carregado. Cada chave é a mesma de PID_IPS_LISTA_SORT_VALOR (não por // acaso: é o extrator de valor bruto reaproveitado tanto pra ordenar // quanto pra filtrar essa coluna) e vale `null` (sem filtro, mostra tudo) // ou um Set com os valores marcados no popup daquela coluna; combinam // entre si (AND) — ex.: empresa "221" + operadora "3755 - Itamed Saúde" // já restringe pras duas condições juntas. let listFiltros = { codigo_empresa: null, nome_operadora: null, status: null, criado_por: null }; 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 = []; // Regras empresa (portal_api.planos_saude.regras_empresa) — registro fixo // no código; regraEmpresaSelecionada guarda {key, label} da regra // atualmente escolhida no checkbox "Mensalidade usa regra especial" (ver // ips-regra-empresa-modal), dentro do formulário de Cadastro de Regras. let regrasEmpresaCache = []; let regraEmpresaSelecionada = null; // Banco de regras de custeio cadastradas por empresa+operadora — fonte // única tanto pra tela "Nova Importação" (só resolve/aplica) quanto pra // "Cadastro de Regras" (edita). let regras = []; // Tela "Nova Importação": a regra resolvida a partir da Empresa+Operadora // escolhidas (no máximo uma por combinação, ver unique_together no // backend) — é o que o submit envia, sem nenhum campo editável na tela. let regraResolvidaAtual = null; // Modal "Cadastro de Regras": empresa/operadora escolhidas no modo // "navegar" (edição de uma regra existente) e a regra resolvida por elas; // regracadModoNovo alterna pro formulário de criação (empresa livre + // operadora sem restrição). let regracadEmpresaAtual = null; let regracadOperadoraAtual = null; let regracadRegraEmEdicao = null; let regracadModoNovo = false; let regracadNovoOperadoraKey = 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 planilhaOrigemAtual() { const marcado = planilhaOrigemRadios.find((r) => r.checked); return marcado ? marcado.value : "questor"; } function atualizarPlanilhaOrigemView() { const questor = planilhaOrigemAtual() === "questor"; planilhaQuestorField.hidden = !questor; planilhaUploadField.hidden = questor; // Trocar de modo limpa o outro — evita enviar um arquivo esquecido // junto com uma competência (ou vice-versa), o que o backend recusaria // mesmo (ImportacaoPlanoSaudeCreateSerializer exige exatamente uma // origem). if (questor) { validadorPlanilha.limpar(); } else { formCompetencia.value = ""; } } planilhaOrigemRadios.forEach((radio) => { radio.addEventListener("change", atualizarPlanilhaOrigemView); }); // Máscara MM/AAAA de digitação — mesmo padrão já usado em // pidIndMascaraCompetencia/pidIndCompetenciaParaIso (indicador-desempenho.js), // copiado aqui em vez de compartilhado (cada arquivo mantém sua própria // cópia dessas funções pequenas, mesmo padrão de comparaValoresUsuario // etc.). Usuário só digita "082026" (ou já com a barra) e o campo forma // "08/2026" sozinho; nunca usar um `` aqui — o // seletor nativo do browser foi considerado horrível pelo usuário. function mascaraCompetencia(valor) { const digitos = (valor || "").replace(/\D/g, "").slice(0, 6); if (digitos.length <= 2) return digitos; return `${digitos.slice(0, 2)}/${digitos.slice(2)}`; } function competenciaParaIso(valor) { const match = /^(\d{2})\/(\d{4})$/.exec(valor || ""); if (!match) return null; const mes = Number(match[1]); if (mes < 1 || mes > 12) return null; return `${match[2]}-${match[1]}-01`; } if (formCompetencia) { formCompetencia.addEventListener("input", (event) => { event.target.value = mascaraCompetencia(event.target.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 || "", }; // Colunas com filtro "estilo Excel" no cabeçalho do histórico — mesma // chave de PID_IPS_LISTA_SORT_VALOR, reaproveitado como extrator do valor // bruto de cada linha pro popup também (ver criarFiltroColuna). "status" // ganha um rótulo amigável no popup (a lista mostra "Em revisão", não // "revisao"), as outras exibem o próprio valor. const PID_IPS_STATUS_LABELS = { revisao: "Em revisão", concluida: "Concluída" }; function labelValorFiltroLista(campo, valor) { if (campo === "status") return PID_IPS_STATUS_LABELS[valor] || valor; return valor || "(Vazio)"; } 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 importacaoPassaNosFiltros(imp) { return Object.entries(listFiltros).every(([campo, valoresPermitidos]) => { if (!valoresPermitidos) return true; // null = sem filtro nessa coluna return valoresPermitidos.has(PID_IPS_LISTA_SORT_VALOR[campo](imp)); }); } function renderList() { const filtradas = importacoes.filter(importacaoPassaNosFiltros); listEmpty.hidden = filtradas.length !== 0; listEmpty.textContent = importacoes.length === 0 ? "Nenhuma importação realizada ainda." : "Nenhuma importação encontrada com esse filtro."; const extrator = PID_IPS_LISTA_SORT_VALOR[listSortKey]; const ordenadas = [...filtradas].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(); } // "Regra empresa" (terceiro tipo de importação, mutuamente exclusivo com // "Mensalidade" — as duas são formas alternativas de configurar o mesmo // tipo de lançamento "mensalidade", ver regras_empresa.py) — zera o // checkbox, esconde a caixa e limpa a seleção em memória. function renderRegraEmpresaAtual() { if (!regraEmpresaAtual) return; regraEmpresaAtual.textContent = regraEmpresaSelecionada ? `Regra selecionada: ${regraEmpresaSelecionada.label}` : "Nenhuma regra selecionada."; } function limparRegraEmpresa() { if (formTipoRegraEmpresa) formTipoRegraEmpresa.checked = false; if (regraEmpresaBox) regraEmpresaBox.hidden = true; regraEmpresaSelecionada = null; renderRegraEmpresaAtual(); } // Zera a parte de tipo/custeio do formulário de Cadastro de Regras // (tipos_lancamento + modo de custeio/limite/percentual de cada // combinação tipo×pessoa) — usado ao entrar no modo "+ Nova regra". function limparCusteioForm() { formTipoMensalidade.checked = false; formTipoCoparticipacao.checked = false; limparRegraEmpresa(); 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 = ""; }); }); } // "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 || formTipoRegraEmpresa.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] = {}; // "Regra empresa" substitui o custeio manual de mensalidade — não há // titular/dependente pra ler aqui (ver regraEmpresaSelecionada). if (tipo === "mensalidade" && formTipoRegraEmpresa.checked) return; 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."; if (formTipoRegraEmpresa.checked && !regraEmpresaSelecionada) { return "Selecione a regra empresa a aplicar."; } for (const tipo of tipos) { if (tipo === "mensalidade" && formTipoRegraEmpresa.checked) continue; 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 : []; // Repõe o custeio de uma regra dentro do formulário de Cadastro de // Regras — mutuamente exclusivo entre "mensalidade manual" e "mensalidade // usa regra especial" (ver limparRegraEmpresa()). limparRegraEmpresa(); if (regra.regra_empresa_chave) { const re = regrasEmpresaCache.find((r) => r.key === regra.regra_empresa_chave); regraEmpresaSelecionada = re || { key: regra.regra_empresa_chave, label: regra.regra_empresa_chave }; if (formTipoRegraEmpresa) formTipoRegraEmpresa.checked = true; if (regraEmpresaBox) regraEmpresaBox.hidden = false; renderRegraEmpresaAtual(); } formTipoMensalidade.checked = tipos.includes("mensalidade") && !regra.regra_empresa_chave; 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; } // Compara códigos de empresa (string) numericamente quando os dois são // numéricos — evita "1006" aparecer antes de "129" por comparação de // string pura (mesmo espírito de comparaValoresLista, dedicado aos // comboboxes de "Empresa" abaixo). function compararCodigoEmpresa(a, b) { const an = Number(a); const bn = Number(b); if (!Number.isNaN(an) && !Number.isNaN(bn)) return an - bn; return String(a).localeCompare(String(b), "pt-BR"); } // " - " quando o nome já foi resolvido do Questor (ver // RegraCusteioPlanoSaudeSerializer.get_nome_empresa) — mesmo padrão que o // combobox de Operadora já usa pro próprio nome. Sem nome resolvido // ainda, mostra só o código (nunca bloqueia navegar/criar regra). function labelEmpresa(codigo) { const comNome = regras.find((r) => r.codigo_empresa === codigo && r.nome_empresa); return comNome ? `${codigo} - ${comNome.nome_empresa}` : codigo; } function empresasComRegra() { const codigos = [...new Set(regras.map((r) => r.codigo_empresa))].sort(compararCodigoEmpresa); return codigos.map((codigo) => ({ key: codigo, label: labelEmpresa(codigo) })); } function operadorasComRegraParaEmpresa(codigoEmpresa) { return regras .filter((r) => r.codigo_empresa === codigoEmpresa) .map((r) => ({ key: r.operadora, label: operadoraLabel(r.operadora) })); } /** * Fábrica genérica de combobox pesquisável (campo de texto dobrando como * "display" do valor selecionado + lista flutuante filtrada por texto, * mesmas classes .ips-combo*) — reaproveitada pelos comboboxes de * Empresa/Operadora de "Nova Importação" (só resolve uma regra já * cadastrada) e de "Cadastro de Regras" (navegar entre regras existentes * ou escolher a operadora de uma regra nova), no lugar do antigo par de * comboboxes hardcoded (Operadora/Regra de custeio salva) que duplicava a * mesma lógica de busca+seleção duas vezes. */ function criarComboboxTexto({ input, list, clearBtn, toggleBtn, onChange }) { let itens = []; const comboEl = input ? input.closest(".ips-combo") : null; function fechar() { if (list) { list.hidden = true; list.innerHTML = ""; } } function renderLista(lista) { if (!list) return; list.innerHTML = lista.length ? lista .map((it) => `
${escapeHtml(it.label)}
`) .join("") : `

Nenhum resultado encontrado.

`; list.hidden = false; } function render(filtro) { const termo = (filtro || "").trim().toLowerCase(); renderLista(termo ? itens.filter((it) => it.label.toLowerCase().includes(termo)) : itens); } // Botão "x" embutido na própria barra do combobox (dentro de // .ips-combo) — só aparece quando há algum texto (digitado ou // selecionado) pra limpar. function atualizarClearBtn() { if (clearBtn) clearBtn.hidden = !(input && input.value.trim()); } function selecionar(item, { silencioso } = {}) { if (input) input.value = item ? item.label : ""; atualizarClearBtn(); fechar(); if (!silencioso && onChange) onChange(item || null); } if (input) { input.addEventListener("focus", () => render(input.value)); input.addEventListener("input", () => { render(input.value); atualizarClearBtn(); if (onChange) onChange(null); }); } if (list) { list.addEventListener("click", (event) => { const el = event.target.closest("[data-combo-key]"); if (!el) return; const item = itens.find((it) => String(it.key) === el.getAttribute("data-combo-key")); if (item) selecionar(item); }); } if (clearBtn) { // Diferente de limpar() (silencioso, usado só por quem já vai tratar // a cascata na mão) — clicar no "x" precisa disparar onChange(null) // normalmente, pra "limpar Empresa também limpa Operadora" continuar // funcionando (ver regracadEmpresaOps.onChange). clearBtn.addEventListener("click", () => { selecionar(null); if (input) input.focus(); }); atualizarClearBtn(); } if (toggleBtn) { // Botão embutido na barra, ao lado do "x" — pedido explícito do // usuário: mostra a lista completa igual a quando o campo está vazio; // se já houver algo selecionado, mostra as OUTRAS opções cadastradas // (sem repetir a já escolhida), já que só focar o campo com um valor // preenchido filtraria pra mostrar de novo só o item já selecionado. toggleBtn.addEventListener("click", () => { if (input) input.focus(); const atual = input ? input.value.trim() : ""; renderLista(atual ? itens.filter((it) => it.label !== atual) : itens); }); } return { setItens(novos) { itens = novos || []; }, selecionar, limpar() { selecionar(null, { silencioso: true }); }, fechar, contains(el) { return !!comboEl && comboEl.contains(el); }, }; } const impOperadoraOps = criarComboboxTexto({ input: impOperadoraSearch, list: impOperadoraList, clearBtn: impOperadoraLimparBtn, toggleBtn: impOperadoraToggleBtn, onChange: (item) => { formOperadora.value = item ? item.key : ""; const empresaAtual = impEmpresaValor.value; regraResolvidaAtual = item ? regras.find((r) => r.codigo_empresa === empresaAtual && r.operadora === item.key) || null : null; renderResumoRegra(regraResolvidaAtual); if (item) validadorArquivo.revalidarSeAnexado(); }, }); const impEmpresaOps = criarComboboxTexto({ input: impEmpresaSearch, list: impEmpresaList, clearBtn: impEmpresaLimparBtn, toggleBtn: impEmpresaToggleBtn, onChange: (item) => { impEmpresaValor.value = item ? item.key : ""; impOperadoraOps.limpar(); if (item) { impOperadoraOps.setItens(operadorasComRegraParaEmpresa(item.key)); if (impOperadoraSearch) impOperadoraSearch.disabled = false; if (impOperadoraToggleBtn) impOperadoraToggleBtn.disabled = false; } else { impOperadoraOps.setItens([]); if (impOperadoraSearch) impOperadoraSearch.disabled = true; if (impOperadoraToggleBtn) impOperadoraToggleBtn.disabled = true; } }, }); // Explica o custeio de uma regra em texto simples pro resumo só-leitura // de "Nova Importação" — nunca editável ali, só exibido. function resumoModoPessoa(entrada) { if (!entrada || !entrada.modo) return "não configurado"; if (entrada.modo === "empresa") return "custeado pela empresa"; if (entrada.modo === "empregado") return "descontado do empregado"; if (entrada.modo === "especifica") { const partes = []; if (entrada.limite_valor !== null && entrada.limite_valor !== undefined) { partes.push(`limite de R$ ${formatarNumeroBr(entrada.limite_valor)}`); } if (entrada.percentual !== null && entrada.percentual !== undefined) { partes.push(`${formatarNumeroBr(entrada.percentual)}% custeado pela empresa`); } return `regra específica (${partes.join(", ") || "sem detalhe"})`; } return entrada.modo; } function renderResumoRegra(regra) { if (!impResumo) return; if (!regra) { impResumo.hidden = true; return; } const tipos = regra.tipos_lancamento || []; const custeioPorTipo = regra.custeio_por_tipo || {}; impResumo.hidden = false; if (impResumoTipos) impResumoTipos.textContent = `Tipos cobertos por esta regra: ${tiposLabel(tipos)}`; if (impResumoMensalidade) { if (tipos.includes("mensalidade")) { if (regra.regra_empresa_chave) { const re = regrasEmpresaCache.find((r) => r.key === regra.regra_empresa_chave); impResumoMensalidade.textContent = `Mensalidade: usa regra especial da empresa — ${re ? re.label : regra.regra_empresa_chave}`; } else { const titular = resumoModoPessoa(custeioPorTipo.mensalidade && custeioPorTipo.mensalidade.titular); const dependente = resumoModoPessoa(custeioPorTipo.mensalidade && custeioPorTipo.mensalidade.dependente); impResumoMensalidade.textContent = `Mensalidade — Titular: ${titular}. Dependente: ${dependente}.`; } impResumoMensalidade.hidden = false; } else { impResumoMensalidade.hidden = true; } } if (impResumoCoparticipacao) { if (tipos.includes("coparticipacao")) { const titular = resumoModoPessoa(custeioPorTipo.coparticipacao && custeioPorTipo.coparticipacao.titular); const dependente = resumoModoPessoa(custeioPorTipo.coparticipacao && custeioPorTipo.coparticipacao.dependente); impResumoCoparticipacao.textContent = `Coparticipação — Titular: ${titular}. Dependente: ${dependente}.`; impResumoCoparticipacao.hidden = false; } else { impResumoCoparticipacao.hidden = true; } } if (impResumoObservacoes) { if (regra.observacoes) { impResumoObservacoes.textContent = `Observações: ${regra.observacoes}`; impResumoObservacoes.hidden = false; } else { impResumoObservacoes.hidden = true; } } } // Monta o FormData de "Nova Importação" a partir da regra resolvida — // mesmo formato de campos que ImportacaoPlanoSaudeCreateSerializer já // espera (custeio__, limite_valor_.../percentual_...), só // que lendo do objeto da regra em memória em vez de inputs editáveis na // tela (que não existem mais aqui). function montarFormDataDeRegra(formData, regra) { formData.append("operadora", regra.operadora); formData.append("tipos_lancamento", (regra.tipos_lancamento || []).join(",")); formData.append("regra_custeio_salva", regra.id); if (regra.regra_empresa_chave) { formData.append("regra_empresa", regra.regra_empresa_chave); } (regra.tipos_lancamento || []).forEach((tipo) => { if (tipo === "mensalidade" && regra.regra_empresa_chave) return; PID_IPS_PESSOAS.forEach((pessoa) => { const entrada = (regra.custeio_por_tipo && regra.custeio_por_tipo[tipo] && regra.custeio_por_tipo[tipo][pessoa]) || {}; formData.append(`custeio_${tipo}_${pessoa}`, entrada.modo || ""); if (entrada.modo === "especifica") { formData.append(`limite_valor_${tipo}_${pessoa}`, formatarNumeroBr(entrada.limite_valor)); formData.append(`percentual_${tipo}_${pessoa}`, formatarNumeroBr(entrada.percentual)); } }); }); } async function refreshRegras() { try { regras = await pidFetchRegrasCusteioPlanoSaude(); } catch (e) { regras = []; } } // Lista do modal "Selecionar regra empresa" — registro fixo em // portal_api.planos_saude.regras_empresa, sem "Excluir"/"Editar" (só o // desenvolvedor cadastra uma regra nova, direto no código). function renderRegraEmpresaLista() { if (!regraEmpresaLista) return; if (!regrasEmpresaCache.length) { regraEmpresaLista.innerHTML = `

Nenhuma regra empresa cadastrada.

`; return; } regraEmpresaLista.innerHTML = regrasEmpresaCache .map( (r) => `

${escapeHtml(r.label)}

` ) .join(""); } // Modal "Cadastro de Regras" — navegar (empresa+operadora resolvem no // máximo uma regra, ver unique_together no backend) ou criar uma nova. const regracadOperadoraOps = criarComboboxTexto({ input: regracadOperadoraSearch, list: regracadOperadoraList, clearBtn: regracadOperadoraLimparBtn, toggleBtn: regracadOperadoraToggleBtn, onChange: (item) => { regracadOperadoraAtual = item ? item.key : null; if (item && regracadEmpresaAtual) { const regra = regras.find((r) => r.codigo_empresa === regracadEmpresaAtual && r.operadora === item.key); if (regra) { regracadCarregarParaEdicao(regra); return; } } regracadRegraEmEdicao = null; regracadMostrarVazio(); }, }); const regracadEmpresaOps = criarComboboxTexto({ input: regracadEmpresaSearch, list: regracadEmpresaList, clearBtn: regracadEmpresaLimparBtn, toggleBtn: regracadEmpresaToggleBtn, onChange: (item) => { regracadEmpresaAtual = item ? item.key : null; regracadOperadoraOps.limpar(); if (item) { regracadOperadoraOps.setItens(operadorasComRegraParaEmpresa(item.key)); if (regracadOperadoraSearch) regracadOperadoraSearch.disabled = false; if (regracadOperadoraToggleBtn) regracadOperadoraToggleBtn.disabled = false; if (regracadNovaOperadoraBtn) regracadNovaOperadoraBtn.hidden = false; } else { regracadOperadoraOps.setItens([]); if (regracadOperadoraSearch) regracadOperadoraSearch.disabled = true; if (regracadOperadoraToggleBtn) regracadOperadoraToggleBtn.disabled = true; if (regracadNovaOperadoraBtn) regracadNovaOperadoraBtn.hidden = true; } regracadRegraEmEdicao = null; regracadMostrarVazio(); }, }); const regracadNovoOperadoraOps = criarComboboxTexto({ input: regracadNovoOperadoraSearch, list: regracadNovoOperadoraList, toggleBtn: regracadNovoOperadoraToggleBtn, onChange: (item) => { regracadNovoOperadoraKey = item ? item.key : null; regracadAtualizarNovoOperadoraDuplicada(); }, }); // Mensagem "Editando regra existente"/"Cadastrando regra nova" acima do // formulário de custeio — pedido explícito do usuário, pra não confundir // os dois modos. `regracadVazio` (nenhum dos dois) não mostra nada. function regracadSetModo(texto, classeExtra) { if (!regracadModo) return; if (!texto) { regracadModo.hidden = true; regracadModo.textContent = ""; regracadModo.className = "ips-regracad-modo"; return; } regracadModo.hidden = false; regracadModo.textContent = texto; regracadModo.className = `ips-regracad-modo ${classeExtra}`; } function regracadMostrarVazio() { if (regracadToolbar) regracadToolbar.hidden = false; if (regracadVazio) regracadVazio.hidden = false; if (regracadNovoView) regracadNovoView.hidden = true; if (regracadFormView) regracadFormView.hidden = true; if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.hidden = true; if (regracadSalvarBtn) regracadSalvarBtn.hidden = true; if (regracadExcluirBtn) regracadExcluirBtn.hidden = true; if (regracadCriarBtn) regracadCriarBtn.hidden = true; if (regracadError) regracadError.textContent = ""; regracadSetModo(null); } function regracadCarregarParaEdicao(regra) { regracadModoNovo = false; regracadRegraEmEdicao = regra; if (regracadToolbar) regracadToolbar.hidden = false; if (regracadNovoView) regracadNovoView.hidden = true; if (regracadVazio) regracadVazio.hidden = true; if (regracadFormView) regracadFormView.hidden = false; aplicarCusteio(regra); if (regracadObservacoes) regracadObservacoes.value = regra.observacoes || ""; if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.hidden = true; if (regracadSalvarBtn) regracadSalvarBtn.hidden = false; if (regracadExcluirBtn) regracadExcluirBtn.hidden = false; if (regracadCriarBtn) regracadCriarBtn.hidden = true; if (regracadError) regracadError.textContent = ""; const empresaLabel = regra.nome_empresa ? `${regra.codigo_empresa} - ${regra.nome_empresa}` : regra.codigo_empresa; regracadSetModo(`Editando regra existente: ${empresaLabel} · ${operadoraLabel(regra.operadora)}`, "ips-regracad-modo--editando"); } // Volta pro estado de navegação (empresa/operadora do topo) a partir do // que já estava selecionado ali — usado por regracadMostrarVazio() (via // onChange dos combos) e por "Cancelar" em "+ Nova regra". function regracadMostrarConformeSelecaoAtual() { if (regracadEmpresaAtual && regracadOperadoraAtual) { const regra = regras.find( (r) => r.codigo_empresa === regracadEmpresaAtual && r.operadora === regracadOperadoraAtual ); if (regra) { regracadCarregarParaEdicao(regra); return; } } regracadMostrarVazio(); } // Nome da empresa (Questor), ao lado do campo "Código da empresa" em "+ // Nova regra" — resolvido/cacheado no backend (ver GET /nome-empresa/), // não bloqueia criar a regra se não encontrar nada. function limparNomeEmpresaNovo() { if (!regracadNovoEmpresaNome) return; regracadNovoEmpresaNome.hidden = true; regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome"; regracadNovoEmpresaNome.textContent = ""; } async function atualizarNomeEmpresaNovo() { if (regracadNomeEmpresaDebounce) { clearTimeout(regracadNomeEmpresaDebounce); regracadNomeEmpresaDebounce = null; } if (!regracadNovoCodigoEmpresa || !regracadNovoEmpresaNome) return; const codigo = regracadNovoCodigoEmpresa.value.trim(); if (!codigo) { limparNomeEmpresaNovo(); return; } regracadNovoEmpresaNome.hidden = false; regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome ips-regracad-novo__empresa-nome--buscando"; regracadNovoEmpresaNome.textContent = "Buscando nome da empresa..."; let resultado = null; try { resultado = await pidBuscarNomeEmpresaPlanoSaude(codigo); } catch (e) { resultado = null; } // O código pode ter mudado enquanto a busca estava em andamento — // nesse caso, quem disparou de novo já está atualizando o texto. if (regracadNovoCodigoEmpresa.value.trim() !== codigo) return; if (resultado && resultado.nome_empresa) { regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome"; regracadNovoEmpresaNome.textContent = resultado.nome_empresa; } else { regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome ips-regracad-novo__empresa-nome--nao-encontrada"; regracadNovoEmpresaNome.textContent = "Empresa não encontrada."; } // O backend só busca/salva o código normalizado (sem zero à esquerda, // ex. "092" -> "92") — reflete isso no campo assim que a resposta // chega, pra nunca ficar exibindo/enviando um código "não-canônico". if (resultado && resultado.codigo_empresa && resultado.codigo_empresa !== codigo) { regracadNovoCodigoEmpresa.value = resultado.codigo_empresa; } regracadAtualizarNovoOperadoraDuplicada(); } // Busca o nome assim que o colaborador para de digitar (debounce), sem // precisar esperar ele clicar em outro campo — pedido explícito do // usuário pra ficar "mais instantâneo". `blur` continua funcionando // também (chama a mesma função direto, sem esperar o debounce). let regracadNomeEmpresaDebounce = null; function agendarAtualizarNomeEmpresaNovo() { if (regracadNomeEmpresaDebounce) clearTimeout(regracadNomeEmpresaDebounce); regracadNomeEmpresaDebounce = setTimeout(atualizarNomeEmpresaNovo, 350); } // Confere se a empresa (campo texto)+operadora (combobox) de "+ Nova // regra" já têm uma regra cadastrada — avisa embaixo da Operadora sem // bloquear "Criar regra" sozinho (o backend também recusa via // unique_together, mas avisar antes evita a viagem de ida e volta). function regracadAtualizarNovoOperadoraDuplicada() { if (!regracadNovoOperadoraDuplicada) return; const codigo = regracadNovoCodigoEmpresa ? regracadNovoCodigoEmpresa.value.trim() : ""; const duplicada = !!codigo && !!regracadNovoOperadoraKey && regras.some((r) => r.codigo_empresa === codigo && r.operadora === regracadNovoOperadoraKey); regracadNovoOperadoraDuplicada.hidden = !duplicada; if (regracadCriarBtn) regracadCriarBtn.disabled = duplicada; } function regracadEntrarModoNovo() { regracadModoNovo = true; regracadRegraEmEdicao = null; regracadNovoOperadoraKey = null; if (regracadNovoCodigoEmpresa) regracadNovoCodigoEmpresa.value = regracadEmpresaAtual || ""; regracadNovoOperadoraOps.setItens(operadorasCache); regracadNovoOperadoraOps.limpar(); limparCusteioForm(); if (regracadObservacoes) regracadObservacoes.value = ""; if (regracadToolbar) regracadToolbar.hidden = true; if (regracadVazio) regracadVazio.hidden = true; if (regracadNovoView) regracadNovoView.hidden = false; if (regracadFormView) regracadFormView.hidden = false; if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.hidden = false; if (regracadSalvarBtn) regracadSalvarBtn.hidden = true; if (regracadExcluirBtn) regracadExcluirBtn.hidden = true; if (regracadCriarBtn) { regracadCriarBtn.hidden = false; regracadCriarBtn.disabled = false; } if (regracadError) regracadError.textContent = ""; regracadSetModo("Cadastrando regra nova", "ips-regracad-modo--novo"); regracadAtualizarNovoOperadoraDuplicada(); if (regracadEmpresaAtual) { atualizarNomeEmpresaNovo(); } else { limparNomeEmpresaNovo(); } } // "Cancelar" em "+ Nova regra" — volta pro topo (Empresa/Operadora) sem // fechar o modal inteiro, diferente de "Fechar". function regracadCancelarNovo() { regracadModoNovo = false; if (regracadNovoView) regracadNovoView.hidden = true; if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.hidden = true; regracadMostrarConformeSelecaoAtual(); } async function abrirCadastroRegras() { regracadModal.hidden = false; if (regracadConfirmFecharModal) regracadConfirmFecharModal.hidden = true; regracadEmpresaAtual = null; regracadOperadoraAtual = null; regracadRegraEmEdicao = null; regracadModoNovo = false; regracadEmpresaOps.limpar(); regracadOperadoraOps.setItens([]); regracadOperadoraOps.limpar(); if (regracadOperadoraSearch) regracadOperadoraSearch.disabled = true; if (regracadOperadoraToggleBtn) regracadOperadoraToggleBtn.disabled = true; if (regracadNovaOperadoraBtn) regracadNovaOperadoraBtn.hidden = true; limparNomeEmpresaNovo(); // Reseta a visualização já aqui, síncrono e antes de qualquer await — // sem isso, o modal reabria mostrando por um instante o que estava na // tela (ex. "Cadastrando regra nova") de antes de fechar, só voltando // ao estado vazio depois que as buscas abaixo terminassem. regracadMostrarVazio(); try { operadorasCache = await pidFetchOperadorasPlanoSaude(); } catch (e) { operadorasCache = []; } try { regrasEmpresaCache = await pidFetchRegrasEmpresaPlanoSaude(); } catch (e) { regrasEmpresaCache = []; } await refreshRegras(); regracadEmpresaOps.setItens(empresasComRegra()); regracadMostrarVazio(); } function fecharCadastroRegras() { regracadModal.hidden = true; } async function openNewForm() { validadorPlanilha.limpar(); validadorArquivo.limpar(); formError.textContent = ""; formCompetencia.value = ""; const radioQuestor = planilhaOrigemRadios.find((r) => r.value === "questor"); if (radioQuestor) radioQuestor.checked = true; atualizarPlanilhaOrigemView(); regraResolvidaAtual = null; renderResumoRegra(null); impEmpresaOps.limpar(); impOperadoraOps.setItens([]); impOperadoraOps.limpar(); if (impOperadoraSearch) impOperadoraSearch.disabled = true; if (impOperadoraToggleBtn) impOperadoraToggleBtn.disabled = true; try { operadorasCache = await pidFetchOperadorasPlanoSaude(); } catch (e) { operadorasCache = []; } try { regrasEmpresaCache = await pidFetchRegrasEmpresaPlanoSaude(); } catch (e) { regrasEmpresaCache = []; } await refreshRegras(); impEmpresaOps.setItens(empresasComRegra()); showView("form"); } function abrirRevisao(importacao) { importacaoAtual = importacao; activeTab = null; if (reviewEmpresa) { if (importacao.codigo_empresa) { reviewEmpresa.textContent = importacao.nome_empresa ? `— ${importacao.codigo_empresa} - ${importacao.nome_empresa}` : `— ${importacao.codigo_empresa}`; reviewEmpresa.hidden = false; } else { reviewEmpresa.hidden = true; reviewEmpresa.textContent = ""; } } const regraEmpresaSufixo = importacao.regra_empresa_label ? ` — Regra empresa: ${importacao.regra_empresa_label}` : ""; // "competencia" só vem preenchida quando a planilha padrão foi buscada // do Questor (não upload manual) — ver ImportacaoPlanoSaude.competencia. const competenciaSufixo = importacao.competencia ? ` — Competência: ${importacao.competencia.slice(5, 7)}/${importacao.competencia.slice(0, 4)}` : ""; reviewSubtitle.textContent = `${importacao.nome_operadora} — criada em ${new Date(importacao.criado_em).toLocaleString("pt-BR")}${regraEmpresaSufixo}${competenciaSufixo}`; if (reviewRegraEmpresaObs) { // As duas regras (empresa e de custeio salva) são mutuamente exclusivas // na criação — nunca as duas preenchidas ao mesmo tempo — mas o bloco é // o mesmo pra qualquer uma que tenha observação cadastrada. if (importacao.regra_empresa_observacoes) { reviewRegraEmpresaObsLabel.textContent = "Observações da regra empresa"; reviewRegraEmpresaObsTexto.textContent = importacao.regra_empresa_observacoes; reviewRegraEmpresaObs.hidden = false; } else if (importacao.regra_custeio_salva_observacoes) { const nome = importacao.regra_custeio_salva_nome ? ` — ${importacao.regra_custeio_salva_nome}` : ""; reviewRegraEmpresaObsLabel.textContent = `Observações da regra de custeio salva${nome}`; reviewRegraEmpresaObsTexto.textContent = importacao.regra_custeio_salva_observacoes; reviewRegraEmpresaObs.hidden = false; } else { reviewRegraEmpresaObs.hidden = true; reviewRegraEmpresaObsTexto.textContent = ""; } } reviewError.textContent = ""; renderTabs(); showView("review"); } function renderTabs() { if (reviewEditarBtn) { reviewEditarBtn.hidden = importacaoAtual.status !== "concluida"; } const tipos = importacaoAtual.tipos_lancamento || []; const tabKeys = [...tipos, "auditoria", "alteracoes"]; if (!activeTab || !tabKeys.includes(activeTab)) activeTab = tabKeys[0]; tabsEl.innerHTML = tabKeys .map((key) => { const label = key === "auditoria" ? "Auditoria" : key === "alteracoes" ? "Alterações" : 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); }); }); } // Colunas sempre editáveis, em qualquer linha — o resto do cadastro // (nome, CPF, código, data...) já vem certo do casamento automático (ou // da busca no Questor) e só devia ser tocado numa linha incluída à mão // (ver linhasIncluidasManualmente() abaixo), nunca numa linha existente // por engano. const PID_IPS_CAMPOS_SEMPRE_EDITAVEIS = ["valor_empresa", "valor"]; // Uma linha é "incluída manualmente" se tiver um ImportacaoPlanoSaudeAlteracao // tipo="inclusao" ainda não revertido apontando pra ela — mesmo sinal que já // existia pra alimentar a aba "Alterações", não precisou de nenhum campo // novo no backend. Só essas linhas ganham os demais campos editáveis; // qualquer linha que já veio do processamento (upload ou Questor) só edita // Valor Empresa/Valor. function linhasIncluidasManualmente() { const ids = new Set(); (importacaoAtual.alteracoes || []).forEach((alt) => { if (alt.tipo === "inclusao" && !alt.revertida && alt.linha) ids.add(alt.linha); }); return ids; } function panelHtmlParaTipo(tipo) { const campos = PID_IPS_CAMPOS_LINHA; // Importação concluída: nada é editável até reabrir (botão "Editar" no // cabeçalho da Revisão) — nem os valores, nem adicionar/remover linha. const concluida = importacaoAtual.status === "concluida"; const incluidasManualmente = linhasIncluidasManualmente(); 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) => { const editavelLivre = !concluida && incluidasManualmente.has(linha.id); const celulasHtml = campos .map(([campo]) => { const editavel = !concluida && (editavelLivre || PID_IPS_CAMPOS_SEMPRE_EDITAVEIS.includes(campo)); return editavel ? `` : `${escapeHtml(linha[campo])}`; }) .join(""); const removerHtml = concluida ? "" : ``; return ` ${removerHtml} ${celulasHtml} `; }) .join(""); return ` ${resumoHtml}
${colGroupHtml(campos, colWidthsLinha, 44)} ${campos .map(([campo, label]) => colHeaderHtml(campo, label, "linha", { key: linhaSortKey, dir: linhaSortDir })) .join("")}${linhasHtml || ``}
Nenhuma linha.
${concluida ? "" : `
`}`; } // Valor sintético (não vem da API) só pra ordenar a coluna "Ação" — // prefixo numérico garante o agrupamento (pendente/resolvido/sem ação) // independente da ordenação alfabética do texto exibido em cada estado. function auditoriaAcaoOrdem(item) { if (item.resolvida) return "1-resolvido"; if (PID_IPS_MOTIVOS_RESOLVIVEIS.includes(item.motivo)) return "0-pendente"; return "2-sem-acao"; } 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) { const valorOrdenacao = auditoriaSortKey === "acao" ? auditoriaAcaoOrdem : (item) => item[auditoriaSortKey]; itens = [...itens].sort( (a, b) => comparaValorRevisao(valorOrdenacao(a), valorOrdenacao(b)) * auditoriaSortDir ); } const linhasHtml = itens .map((item) => { let acaoHtml = `—`; if (item.resolvida) { acaoHtml = `Resolvido${ item.linha_vinculada_nome ? ` — ${item.linha_vinculada_nome}` : "" }`; } else if (importacaoAtual.status !== "concluida" && 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 ↕
`; } // Aba "Alterações" — histórico de edição/inclusão/exclusão de linha feito // manualmente na revisão (ImportacaoPlanoSaudeAlteracao, ver models.py). Já // vem do backend ordenado do mais recente pro mais antigo; sem ordenação/ // redimensionamento de coluna (é um log, não uma planilha editável). function panelHtmlAlteracoes() { const alteracoes = importacaoAtual.alteracoes || []; if (!alteracoes.length) { return `

Nenhuma alteração registrada nesta importação ainda.

`; } const linhasHtml = alteracoes .map((alt) => { let detalheHtml; if (alt.tipo === "edicao") { const campoLabel = PID_IPS_CAMPO_LABELS[alt.campo] || alt.campo; detalheHtml = `${escapeHtml(campoLabel)}: "${escapeHtml(alt.valor_anterior)}" → "${escapeHtml(alt.valor_novo)}"`; } else if (alt.tipo === "inclusao") { detalheHtml = "Linha incluída manualmente"; } else { detalheHtml = "Linha removida"; } const acaoHtml = alt.revertida ? `Revertida` : importacaoAtual.status === "concluida" ? `—` : ``; return ` ${acaoHtml} ${new Date(alt.criado_em).toLocaleString("pt-BR")} ${PID_IPS_ALTERACAO_TIPO_LABELS[alt.tipo] || alt.tipo} ${PID_IPS_TIPO_LABELS[alt.tipo_lancamento] || alt.tipo_lancamento || "—"} ${alt.linha_nome ? escapeHtml(alt.linha_nome) : "—"} ${detalheHtml} ${alt.usuario_nome ? escapeHtml(alt.usuario_nome) : "—"} `; }) .join(""); return `
${linhasHtml}
Ação Quando Tipo Lançamento Linha Alteração Usuário
`; } function renderPanels() { const tipos = importacaoAtual.tipos_lancamento || []; panelsEl.innerHTML = [...tipos, "auditoria", "alteracoes"] .map((key) => { const html = key === "auditoria" ? panelHtmlAuditoria() : key === "alteracoes" ? panelHtmlAlteracoes() : 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 (reviewEditarBtn) { reviewEditarBtn.addEventListener("click", async () => { reviewEditarBtn.disabled = true; try { importacaoAtual = await pidReabrirImportacaoPlanoSaude(importacaoAtual.id); reviewError.textContent = ""; renderTabs(); } catch (e) { reviewError.textContent = e.message; } finally { reviewEditarBtn.disabled = false; } }); } if (regracadBtn) regracadBtn.addEventListener("click", abrirCadastroRegras); if (regracadFecharBtn) regracadFecharBtn.addEventListener("click", fecharCadastroRegras); if (regracadModal) { regracadModal.addEventListener("click", (event) => { // Diferente dos outros modais do app: este formulário costuma ter // edição em andamento (custeio, observações) — clicar fora por // engano não deve descartar isso sem perguntar. Usa um modal próprio // (mesmo padrão visual de .modal-overlay/.modal-card do resto do // app) em vez de window.confirm() — o diálogo nativo do browser // aparece com a URL do servidor ("192.168.x.x:8000 diz..."), fora do // visual do Portal. if (event.target === regracadModal && regracadConfirmFecharModal) { regracadConfirmFecharModal.hidden = false; } }); } if (regracadConfirmFecharCancelarBtn) { regracadConfirmFecharCancelarBtn.addEventListener("click", () => { regracadConfirmFecharModal.hidden = true; }); } if (regracadConfirmFecharOkBtn) { regracadConfirmFecharOkBtn.addEventListener("click", () => { regracadConfirmFecharModal.hidden = true; fecharCadastroRegras(); }); } if (regracadConfirmFecharModal) { // Clicar fora do próprio diálogo de confirmação cancela (não fecha o // cadastro) — mesmo espírito conservador do resto do app. regracadConfirmFecharModal.addEventListener("click", (event) => { if (event.target === regracadConfirmFecharModal) regracadConfirmFecharModal.hidden = true; }); } if (regracadNovaBtn) regracadNovaBtn.addEventListener("click", regracadEntrarModoNovo); if (regracadNovaOperadoraBtn) regracadNovaOperadoraBtn.addEventListener("click", regracadEntrarModoNovo); if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.addEventListener("click", regracadCancelarNovo); if (regracadNovoCodigoEmpresa) { regracadNovoCodigoEmpresa.addEventListener("input", agendarAtualizarNomeEmpresaNovo); regracadNovoCodigoEmpresa.addEventListener("blur", atualizarNomeEmpresaNovo); } document.addEventListener("click", (event) => { [impEmpresaOps, impOperadoraOps, regracadEmpresaOps, regracadOperadoraOps, regracadNovoOperadoraOps].forEach( (combo) => { if (combo && !combo.contains(event.target)) combo.fechar(); } ); }); if (formTipoMensalidade) { formTipoMensalidade.addEventListener("change", () => { custeioMensalidade.hidden = !formTipoMensalidade.checked; // Mutuamente exclusivo com "Regra empresa" — as duas são formas // alternativas de configurar o mesmo tipo de lançamento "mensalidade". if (formTipoMensalidade.checked) limparRegraEmpresa(); }); } if (formTipoCoparticipacao) { formTipoCoparticipacao.addEventListener("change", () => { custeioCoparticipacao.hidden = !formTipoCoparticipacao.checked; }); } if (formTipoRegraEmpresa) { formTipoRegraEmpresa.addEventListener("change", () => { if (regraEmpresaBox) regraEmpresaBox.hidden = !formTipoRegraEmpresa.checked; if (formTipoRegraEmpresa.checked) { // Mesma exclusividade mútua do outro lado (formTipoMensalidade acima). formTipoMensalidade.checked = false; custeioMensalidade.hidden = true; custeioMensalidade.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false)); } else { regraEmpresaSelecionada = null; renderRegraEmpresaAtual(); } }); } if (regraEmpresaSelecionarBtn) { regraEmpresaSelecionarBtn.addEventListener("click", () => { if (regraEmpresaError) regraEmpresaError.textContent = ""; renderRegraEmpresaLista(); if (regraEmpresaModal) regraEmpresaModal.hidden = false; }); } if (regraEmpresaLista) { regraEmpresaLista.addEventListener("click", (event) => { const btn = event.target.closest("[data-regra-empresa-selecionar]"); if (!btn) return; const key = btn.getAttribute("data-regra-empresa-selecionar"); const regra = regrasEmpresaCache.find((r) => r.key === key); if (!regra) return; regraEmpresaSelecionada = regra; renderRegraEmpresaAtual(); if (regraEmpresaModal) regraEmpresaModal.hidden = true; }); } if (regraEmpresaCancelBtn) regraEmpresaCancelBtn.addEventListener("click", () => (regraEmpresaModal.hidden = true)); if (regraEmpresaModal) { regraEmpresaModal.addEventListener("click", (event) => { if (event.target === regraEmpresaModal) regraEmpresaModal.hidden = true; }); } if (regracadSalvarBtn) { regracadSalvarBtn.addEventListener("click", async () => { if (!regracadRegraEmEdicao) return; const { tipos_lancamento, custeio_por_tipo } = coletarCusteioAtual(); const erro = mensagemErroCusteio(tipos_lancamento); if (erro) { regracadError.textContent = erro; return; } const payload = { codigo_empresa: regracadEmpresaAtual, operadora: regracadOperadoraAtual, regra_empresa_chave: formTipoRegraEmpresa.checked && regraEmpresaSelecionada ? regraEmpresaSelecionada.key : "", tipos_lancamento, custeio_por_tipo, observacoes: regracadObservacoes ? regracadObservacoes.value.trim() : "", }; regracadSalvarBtn.disabled = true; try { await pidAtualizarRegraCusteioPlanoSaude(regracadRegraEmEdicao.id, payload); await refreshRegras(); // Pedido explícito do usuário: depois de salvar com sucesso, fecha // o modal em vez de continuar mostrando a regra editada. fecharCadastroRegras(); } catch (e) { regracadError.textContent = e.message; } finally { regracadSalvarBtn.disabled = false; } }); } if (regracadExcluirBtn) { regracadExcluirBtn.addEventListener("click", async () => { if (!regracadRegraEmEdicao) return; if (!window.confirm(`Excluir a regra "${regracadRegraEmEdicao.nome}"? Essa ação não pode ser desfeita.`)) return; regracadExcluirBtn.disabled = true; try { await pidExcluirRegraCusteioPlanoSaude(regracadRegraEmEdicao.id); await refreshRegras(); regracadEmpresaOps.setItens(empresasComRegra()); regracadOperadoraOps.setItens(regracadEmpresaAtual ? operadorasComRegraParaEmpresa(regracadEmpresaAtual) : []); regracadOperadoraOps.limpar(); regracadRegraEmEdicao = null; regracadMostrarVazio(); } catch (e) { regracadError.textContent = e.message; } finally { regracadExcluirBtn.disabled = false; } }); } if (regracadCriarBtn) { regracadCriarBtn.addEventListener("click", async () => { const codigoEmpresa = regracadNovoCodigoEmpresa ? regracadNovoCodigoEmpresa.value.trim() : ""; if (!codigoEmpresa) { regracadError.textContent = "Informe o código da empresa."; return; } if (!regracadNovoOperadoraKey) { regracadError.textContent = "Selecione a operadora."; return; } const { tipos_lancamento, custeio_por_tipo } = coletarCusteioAtual(); const erro = mensagemErroCusteio(tipos_lancamento); if (erro) { regracadError.textContent = erro; return; } const payload = { codigo_empresa: codigoEmpresa, operadora: regracadNovoOperadoraKey, regra_empresa_chave: formTipoRegraEmpresa.checked && regraEmpresaSelecionada ? regraEmpresaSelecionada.key : "", tipos_lancamento, custeio_por_tipo, observacoes: regracadObservacoes ? regracadObservacoes.value.trim() : "", }; regracadCriarBtn.disabled = true; try { await pidCriarRegraCusteioPlanoSaude(payload); await refreshRegras(); // Pedido explícito do usuário: depois de criar com sucesso, fecha // o modal em vez de continuar mostrando a regra recém-criada. fecharCadastroRegras(); } catch (e) { regracadError.textContent = e.message; } finally { regracadCriarBtn.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); return; } const reverterBtn = event.target.closest("[data-reverter-alteracao]"); if (reverterBtn) { const alteracaoId = Number(reverterBtn.getAttribute("data-reverter-alteracao")); if (!window.confirm("Reverter esta alteração?")) return; reverterBtn.disabled = true; try { await pidReverterAlteracaoPlanoSaude(alteracaoId); importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id); reviewError.textContent = ""; renderTabs(); } catch (e) { reviewError.textContent = e.message; reverterBtn.disabled = false; } } }); } if (gerarBtn) { gerarBtn.addEventListener("click", async () => { reviewError.textContent = ""; gerarBtn.disabled = true; try { await pidGerarArquivoPlanoSaude(importacaoAtual.id); // Depois de baixar o arquivo, volta pro histórico — mesmo pedido do // usuário pro fluxo de "Nova Importação" (abrirRevisao já leva pra // lá), já que gerar() marca a importação como concluída e não há // mais nada a revisar imediatamente depois de baixar o arquivo. showView("list"); await refreshList(); } 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", (event) => { // Ignora cliques que vieram de dentro do popup de filtro (ver // criarFiltroColuna abaixo) — sem isso, abrir o filtro ou marcar um // checkbox dele também reordenava a coluna, já que o filtro mora // dentro do mesmo clicável. if (event.target.closest(".ips-col-filter")) return; const key = th.getAttribute("data-sort"); if (listSortKey === key) { listSortDir *= -1; } else { listSortKey = key; listSortDir = 1; } listPage = 1; renderList(); }); }); // Filtro "estilo Excel" por coluna do histórico — botão de funil dentro // do próprio de ordenação, popup com busca + checklist de valores // distintos (reaproveita .checklist-* de components.css). `filtrosColuna` // guarda um `fechar()` por coluna, pra fechar todos ao abrir outro ou ao // clicar fora de qualquer um deles. const filtrosColuna = []; function fecharTodosOsFiltrosColuna() { filtrosColuna.forEach((f) => f.fechar()); } function criarFiltroColuna(campo) { const th = listSortHeaders.find((el) => el.getAttribute("data-sort") === campo); if (!th) return; const wrapper = document.createElement("span"); wrapper.className = "ips-col-filter"; wrapper.innerHTML = ` `; th.appendChild(wrapper); const btn = wrapper.querySelector(".ips-col-filter__btn"); const popup = wrapper.querySelector(".ips-col-filter__popup"); const searchInput = wrapper.querySelector(".checklist-search"); const selectAllInput = wrapper.querySelector(".checklist-select-all input"); const box = wrapper.querySelector(".checklist-box"); const limparBtn = wrapper.querySelector(".ips-col-filter__limpar"); const aplicarBtn = wrapper.querySelector(".ips-col-filter__aplicar"); const extrator = PID_IPS_LISTA_SORT_VALOR[campo]; let marcados = null; // Set em edição — só existe enquanto o popup está aberto function valoresDistintos() { return Array.from(new Set(importacoes.map(extrator))).sort((a, b) => labelValorFiltroLista(campo, a).localeCompare(labelValorFiltroLista(campo, b), "pt-BR", { sensitivity: "base" }) ); } function renderItens() { const termo = searchInput.value.trim().toLowerCase(); const visiveis = valoresDistintos().filter((v) => labelValorFiltroLista(campo, v).toLowerCase().includes(termo)); box.innerHTML = visiveis.length ? visiveis .map( (v) => `` ) .join("") : `

Nenhum valor encontrado.

`; selectAllInput.checked = visiveis.length > 0 && visiveis.every((v) => marcados.has(v)); } function abrir() { // `marcados` sempre recomeça a partir do filtro já aplicado (ou de // todos os valores existentes, se a coluna ainda não tem filtro) — // nunca do que ficou de uma edição anterior cancelada sem "Aplicar". marcados = listFiltros[campo] ? new Set(listFiltros[campo]) : new Set(valoresDistintos()); searchInput.value = ""; renderItens(); popup.hidden = false; btn.setAttribute("aria-expanded", "true"); } function fechar() { popup.hidden = true; marcados = null; btn.setAttribute("aria-expanded", "false"); } function atualizarBotaoAtivo() { btn.classList.toggle("is-active", !!listFiltros[campo]); } btn.addEventListener("click", (event) => { event.stopPropagation(); const estavaAberto = !popup.hidden; fecharTodosOsFiltrosColuna(); if (!estavaAberto) abrir(); }); popup.addEventListener("click", (event) => event.stopPropagation()); searchInput.addEventListener("input", renderItens); selectAllInput.addEventListener("change", () => { const termo = searchInput.value.trim().toLowerCase(); const visiveis = valoresDistintos().filter((v) => labelValorFiltroLista(campo, v).toLowerCase().includes(termo)); if (selectAllInput.checked) visiveis.forEach((v) => marcados.add(v)); else visiveis.forEach((v) => marcados.delete(v)); renderItens(); }); box.addEventListener("change", (event) => { const input = event.target.closest("[data-valor-filtro]"); if (!input) return; const valor = input.getAttribute("data-valor-filtro"); if (input.checked) marcados.add(valor); else marcados.delete(valor); renderItens(); }); limparBtn.addEventListener("click", () => { listFiltros[campo] = null; fechar(); atualizarBotaoAtivo(); listPage = 1; renderList(); }); aplicarBtn.addEventListener("click", () => { const todos = valoresDistintos(); // Marcar tudo equivale a "sem filtro" — mantém null nesse caso, senão // um valor novo que apareça depois (operadora nova, por exemplo) // ficaria de fora até o usuário marcá-lo manualmente de novo. const marcouTudo = todos.length === marcados.size && todos.every((v) => marcados.has(v)); listFiltros[campo] = marcouTudo ? null : new Set(marcados); fechar(); atualizarBotaoAtivo(); listPage = 1; renderList(); }); filtrosColuna.push({ fechar }); } ["codigo_empresa", "nome_operadora", "status", "criado_por"].forEach(criarFiltroColuna); document.addEventListener("click", (event) => { if (!event.target.closest(".ips-col-filter")) fecharTodosOsFiltrosColuna(); }); 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 () => { if (!regraResolvidaAtual) { formError.textContent = "Selecione a empresa e a operadora."; return; } const origemQuestor = planilhaOrigemAtual() === "questor"; let competenciaIso = null; if (origemQuestor) { competenciaIso = competenciaParaIso(formCompetencia.value); if (!competenciaIso) { formError.textContent = "Informe a competência no formato MM/AAAA para buscar a planilha no Questor."; return; } } else { if (!formPlanilha.files[0]) { formError.textContent = "Anexe a planilha padrão do Questor."; return; } if (validadorPlanilha.ehInvalido()) { formError.textContent = "Corrija a planilha anexada (veja a mensagem abaixo do campo) antes de processar."; return; } } if (!formArquivo.files[0]) { formError.textContent = "Anexe o arquivo da operadora."; return; } if (validadorArquivo.ehInvalido()) { formError.textContent = "Corrija o arquivo da operadora (veja a mensagem abaixo do campo) antes de processar."; return; } formError.textContent = ""; const formData = new FormData(); if (origemQuestor) { formData.append("competencia", competenciaIso); } else { formData.append("planilha_padrao", formPlanilha.files[0]); } formData.append("arquivo_operadora", formArquivo.files[0]); montarFormDataDeRegra(formData, regraResolvidaAtual); 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(); });