portal_publico/static/js/importacao-plano-saude.js

2842 lines
122 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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",
};
// Larguras padrão ajustadas (pedido explícito do usuário) pra caber cada
// rótulo do cabeçalho sem cortar, sem precisar redimensionar manualmente
// toda vez que abre uma importação — continuam ajustáveis pela alça de
// redimensionar (colWidthsLinha, ver wireColumnResize), isso só muda o
// ponto de partida.
const PID_IPS_CAMPOS_LINHA = [
["codigo_empresa", "Cód. Empresa", 110],
["nome_func", "Nome Funcionário", 210],
["cpf_func", "CPF Funcionário", 160],
["codigo_out_emp", "Cód. Out. Emp.", 140],
["data_inicial", "Data Inicial", 140],
["nome_dependente", "Nome Dependente", 210],
["cpf_dependente", "CPF Dependente", 160],
["valor_empresa", "Valor Empresa", 130],
["valor", "Valor", 100],
["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]));
// Só os dois campos monetários aceitam expressão de soma/subtração digitada
// na própria célula (ex.: "15,30-15" -> "0,30") — pedido explícito do
// usuário, pra abater/somar um valor sem precisar calcular o resultado à
// parte antes de digitar. Nome/CPF/data/descrição nunca passam por aqui.
const PID_IPS_CAMPOS_EXPRESSAO_VALOR = new Set(["valor_empresa", "valor"]);
// Devolve o valor calculado (formato BR, "0,30") se `texto` for uma
// expressão reconhecível de soma/subtração entre números BR (vírgula
// decimal, ponto de milhar opcional), ou `null` se não for — nesse caso o
// valor digitado segue para o servidor exatamente como está, sem alteração
// nenhuma (mesmo comportamento de antes desta função existir).
//
// Um único número com sinal de abertura (ex.: "-15,30") NÃO conta como
// expressão — só quando há pelo menos um operador +/- depois do primeiro
// caractere, senão todo valor negativo digitado direto viraria "0" (soma de
// um termo só). Qualquer caractere que sobre fora dos termos reconhecidos
// (letra, símbolo, dois operadores seguidos...) também devolve `null`, pra
// nunca "adivinhar" um cálculo a partir de um valor mal digitado.
function pidAvaliaExpressaoValorMonetario(texto) {
const raw = (texto || "").trim();
if (!raw || raw.slice(1).search(/[+-]/) === -1) return null;
const semEspacos = raw.replace(/\s+/g, "");
const termos = semEspacos.match(/[+-]?\d+(?:\.\d{3})*(?:,\d+)?/g);
if (!termos || termos.join("") !== semEspacos) return null;
const soma = termos.reduce((total, termo) => {
const sinal = termo.startsWith("-") ? -1 : 1;
const numero = termo.replace(/^[+-]/, "").replace(/\./g, "").replace(",", ".");
return total + sinal * parseFloat(numero);
}, 0);
if (!Number.isFinite(soma)) return null;
return soma === 0 ? "0" : soma.toFixed(2).replace(".", ",");
}
// Espelha ImportacaoPlanoSaudeAlteracao.TIPO_CHOICES (models.py).
const PID_IPS_ALTERACAO_TIPO_LABELS = {
edicao: "Edição",
inclusao: "Inclusão",
exclusao: "Exclusão",
vinculo_automatico: "Vínculo automático",
};
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;
pidCriarBotaoAjuda("ips-ajuda-btn", "importacao-plano-saude", "Importação de Plano de Saúde");
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 formArquivoList = document.getElementById("ips-form-arquivo-list");
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 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, tipos_lancamento}
// da regra atualmente escolhida no checkbox "Regra especial da empresa"
// (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",
});
/*
* "2. Arquivo da operadora" aceita 1+ arquivos (algumas operadoras mandam
* mensalidade e coparticipação em arquivos separados) — cada arquivo
* anexado é validado individualmente (mesmo endpoint de sempre,
* pidValidarArquivoPlanoSaude, chamado uma vez por arquivo) e listado com
* seu próprio status + botão de remover. Diferente de
* criarValidadorArquivo() acima (um único arquivo, pensado pra planilha
* padrão), este mantém um array de {file, resultado} e re-renderiza a
* lista a cada mudança.
*/
function criarValidadorArquivoMultiplo({ input, listEl, tipo, getOperadoraKey }) {
let arquivos = [];
function render() {
listEl.innerHTML = "";
arquivos.forEach((item, index) => {
const li = document.createElement("li");
li.className = "ips-arquivo-list__item";
const info = document.createElement("div");
info.className = "ips-arquivo-list__info";
const nameEl = document.createElement("span");
nameEl.className = "ips-arquivo-list__name";
nameEl.textContent = item.file.name;
const statusEl = document.createElement("span");
if (item.resultado === null) {
statusEl.className = "ips-arquivo-list__status ips-arquivo-list__status--pendente";
statusEl.textContent = "Validando arquivo...";
} else {
statusEl.className = `ips-arquivo-list__status ${item.resultado.valido ? "ips-arquivo-list__status--ok" : "ips-arquivo-list__status--erro"}`;
statusEl.textContent = `${item.resultado.valido ? "✓" : "✗"} ${item.resultado.mensagem}`;
}
info.appendChild(nameEl);
info.appendChild(statusEl);
const removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.className = "icon-btn ips-arquivo-list__remove";
removeBtn.setAttribute("aria-label", "Remover arquivo");
removeBtn.title = "Remover arquivo";
removeBtn.innerHTML =
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>';
removeBtn.addEventListener("click", () => {
arquivos.splice(index, 1);
render();
});
li.appendChild(info);
li.appendChild(removeBtn);
listEl.appendChild(li);
});
}
async function validarItem(item) {
item.resultado = null;
render();
try {
const operadoraKey = getOperadoraKey ? getOperadoraKey() : null;
item.resultado = await pidValidarArquivoPlanoSaude(tipo, item.file, operadoraKey);
} catch (e) {
item.resultado = { valido: false, mensagem: e.message };
}
render();
}
function limpar() {
input.value = "";
arquivos = [];
render();
}
input.addEventListener("change", () => {
const novosArquivos = Array.from(input.files || []).map((file) => ({ file, resultado: null }));
input.value = "";
arquivos = arquivos.concat(novosArquivos);
render();
novosArquivos.forEach(validarItem);
});
return {
limpar,
revalidarSeAnexado: () => arquivos.forEach(validarItem),
ehVazio: () => arquivos.length === 0,
ehInvalido: () => arquivos.some((item) => item.resultado !== null && item.resultado.valido === false),
arquivosSelecionados: () => arquivos.map((item) => item.file),
};
}
const validadorArquivo = criarValidadorArquivoMultiplo({
input: formArquivo,
listEl: formArquivoList,
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 `<input type="month">` 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 = `
<button type="button" class="btn-outline" id="ips-list-prev-btn" ${listPage === 1 ? "disabled" : ""}>Página anterior</button>
<span class="ips-list-pagination__info">Página ${listPage} de ${totalPaginas}</span>
<button type="button" class="btn-outline" id="ips-list-next-btn" ${listPage === totalPaginas ? "disabled" : ""}>Próxima página</button>`;
}
// "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 = `
<td class="ips-select-col"><input type="checkbox" class="ips-select-row" data-select-id="${imp.id}" aria-label="Selecionar esta importação" ${selectedIds.has(imp.id) ? "checked" : ""} /></td>
<td>${imp.codigo_empresa || "—"}</td>
<td>${imp.nome_operadora}</td>
<td>${tiposLabel(imp.tipos_lancamento)}</td>
<td><span class="status-pill ${statusClasse}">${statusTexto}</span></td>
<td>${imp.criado_por_nome || "—"}</td>
<td>${criadoEm}</td>
<td class="ips-table-actions">
<button type="button" class="icon-btn ips-remove-linha" data-remover-importacao="${imp.id}" aria-label="Excluir importação" title="Excluir importação">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
</td>`;
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 especial da empresa" ("Regra empresa" — cobre um subconjunto de
// tipos de lançamento, mensalidade e/ou coparticipação conforme a regra
// escolhida, 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.";
}
// Tipos de lançamento efetivamente cobertos agora mesmo pela regra
// especial selecionada — [] se o checkbox estiver desmarcado ou nenhuma
// regra tiver sido escolhida ainda (ver GET .../regras-empresa/, que
// devolve tipos_lancamento por regra).
function regraEmpresaTiposAtuais() {
if (!formTipoRegraEmpresa.checked || !regraEmpresaSelecionada) return [];
return regraEmpresaSelecionada.tipos_lancamento || [];
}
// Trava (marca + desabilita + esconde o custeio) o checkbox de cada tipo
// coberto pela regra especial atual, e libera de volta (desmarca +
// reabilita) qualquer tipo que estava travado por uma regra anterior mas
// deixou de estar coberto (regra trocada, ou "Regra especial" desmarcada)
// — chamada sempre que a seleção de regra muda. Mutuamente exclusivo por
// TIPO, não em bloco: uma regra que só cobre mensalidade (ex.: Tecnomyl)
// deixa "Coparticipação" livre pra configuração manual normalmente.
function aplicarTiposRegraEmpresa() {
const tipos = regraEmpresaTiposAtuais();
[
{ checkbox: formTipoMensalidade, custeio: custeioMensalidade, tipo: "mensalidade" },
{ checkbox: formTipoCoparticipacao, custeio: custeioCoparticipacao, tipo: "coparticipacao" },
].forEach(({ checkbox, custeio, tipo }) => {
if (!checkbox) return;
if (tipos.includes(tipo)) {
checkbox.checked = true;
checkbox.disabled = true;
custeio.hidden = true;
custeio.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false));
} else if (checkbox.disabled) {
checkbox.checked = false;
checkbox.disabled = false;
custeio.hidden = true;
}
});
}
function limparRegraEmpresa() {
if (formTipoRegraEmpresa) formTipoRegraEmpresa.checked = false;
if (regraEmpresaBox) regraEmpresaBox.hidden = true;
regraEmpresaSelecionada = null;
renderRegraEmpresaAtual();
aplicarTiposRegraEmpresa();
}
// 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;
formTipoMensalidade.disabled = false;
formTipoCoparticipacao.checked = false;
formTipoCoparticipacao.disabled = 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 tiposRegraEmpresa = regraEmpresaTiposAtuais();
const tipos_lancamento = [];
if (formTipoMensalidade.checked || tiposRegraEmpresa.includes("mensalidade")) tipos_lancamento.push("mensalidade");
if (formTipoCoparticipacao.checked || tiposRegraEmpresa.includes("coparticipacao")) tipos_lancamento.push("coparticipacao");
const custeio_por_tipo = {};
PID_IPS_TIPOS.forEach((tipo) => {
custeio_por_tipo[tipo] = {};
// "Regra especial da empresa" substitui o custeio manual dos tipos
// que ela cobre — não há titular/dependente pra ler aqui pra esses
// tipos (ver regraEmpresaTiposAtuais()).
if (tiposRegraEmpresa.includes(tipo)) 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 especial a aplicar.";
}
const tiposRegraEmpresa = regraEmpresaTiposAtuais();
for (const tipo of tipos) {
if (tiposRegraEmpresa.includes(tipo)) 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 — a regra especial (se houver) trava os tipos que ela cobre,
// os demais tipos selecionados seguem custeio manual (ver
// aplicarTiposRegraEmpresa()).
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, tipos_lancamento: tipos };
if (formTipoRegraEmpresa) formTipoRegraEmpresa.checked = true;
if (regraEmpresaBox) regraEmpresaBox.hidden = false;
renderRegraEmpresaAtual();
}
const tiposRegraEmpresa = regraEmpresaTiposAtuais();
aplicarTiposRegraEmpresa();
// aplicarTiposRegraEmpresa() já cuidou dos tipos cobertos pela regra
// especial (checked+disabled+custeio escondido) — só os tipos NÃO
// cobertos ainda precisam refletir o `tipos_lancamento` salvo aqui.
if (!tiposRegraEmpresa.includes("mensalidade")) {
formTipoMensalidade.checked = tipos.includes("mensalidade");
custeioMensalidade.hidden = !formTipoMensalidade.checked;
}
if (!tiposRegraEmpresa.includes("coparticipacao")) {
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");
}
// "<código> - <nome>" 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) => `<div class="ips-combo__item" data-combo-key="${escapeAttr(String(it.key))}">${escapeHtml(it.label)}</div>`)
.join("")
: `<p class="ips-combo__empty">Nenhum resultado encontrado.</p>`;
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;
}
// Tipos de lançamento cobertos pela regra especial de código `chave`
// (busca em regrasEmpresaCache, já carregado de GET .../regras-empresa/)
// — [] se `chave` for vazia ou não encontrada.
function tiposCobertosPorRegraEmpresaChave(chave) {
if (!chave) return [];
const re = regrasEmpresaCache.find((r) => r.key === chave);
return (re && re.tipos_lancamento) || [];
}
function renderResumoRegra(regra) {
if (!impResumo) return;
if (!regra) {
impResumo.hidden = true;
return;
}
const tipos = regra.tipos_lancamento || [];
const custeioPorTipo = regra.custeio_por_tipo || {};
const tiposRegraEmpresa = tiposCobertosPorRegraEmpresaChave(regra.regra_empresa_chave);
impResumo.hidden = false;
if (impResumoTipos) impResumoTipos.textContent = `Tipos cobertos por esta regra: ${tiposLabel(tipos)}`;
if (impResumoMensalidade) {
if (tipos.includes("mensalidade")) {
if (tiposRegraEmpresa.includes("mensalidade")) {
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")) {
if (tiposRegraEmpresa.includes("coparticipacao")) {
const re = regrasEmpresaCache.find((r) => r.key === regra.regra_empresa_chave);
impResumoCoparticipacao.textContent = `Coparticipação: usa regra especial da empresa — ${re ? re.label : regra.regra_empresa_chave}`;
} else {
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_<tipo>_<pessoa>, 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);
}
const tiposRegraEmpresa = tiposCobertosPorRegraEmpresaChave(regra.regra_empresa_chave);
(regra.tipos_lancamento || []).forEach((tipo) => {
if (tiposRegraEmpresa.includes(tipo)) 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 = `<p class="checklist-empty">Nenhuma regra empresa cadastrada.</p>`;
return;
}
regraEmpresaLista.innerHTML = regrasEmpresaCache
.map(
(r) => `
<div class="ips-regra-row">
<div class="ips-regra-row__info">
<p class="ips-regra-row__nome">${escapeHtml(r.label)}</p>
</div>
<div class="ips-regra-row__actions">
<button type="button" class="btn-outline" data-regra-empresa-selecionar="${r.key}">Selecionar</button>
</div>
</div>`
)
.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;
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 `<button type="button" class="pa-tab${key === activeTab ? " is-active" : ""}" data-ips-tab="${key}">${label}</button>`;
})
.join("");
renderPanels();
}
function escapeAttr(value) {
return (value === null || value === undefined ? "" : String(value)).replace(/"/g, "&quot;");
}
// 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]
);
}
function resizeHandleHtml(campo, target) {
return `<span class="ips-col-resize-handle" data-resize-campo="${campo}" data-resize-target="${target}"></span>`;
}
// Monta o `grid-template-columns` do .ips-grid a partir dos mesmos pares
// [campo, label, largura] de sempre — ver a nota grande em wireColumnResize
// (abaixo) sobre por que essa tabela usa CSS Grid em vez de <table>.
function gridTemplateColumns(campos, widths, extraColWidth) {
const extra = extraColWidth ? `${extraColWidth}px ` : "";
const cols = campos.map(([campo, , largura]) => `${widths[campo] || largura}px`).join(" ");
return `${extra}${cols}`;
}
// 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. A largura em si
// não é mais responsabilidade da célula — vem do `grid-template-columns`
// do container `.ips-grid` (ver gridTemplateColumns() acima).
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 `<div class="ips-grid-cell ips-grid-cell--head ips-col-sortable${sortClasse}" role="columnheader" data-col-campo="${campo}" data-col-target="${target}">${label} <span class="ips-sort-icon">↕</span>${resizeHandleHtml(campo, target)}</div>`;
}
// Tabela de Mensalidade/Coparticipação/Auditoria é montada com CSS Grid
// (.ips-grid/.ips-grid-row/.ips-grid-cell, importacao-plano-saude.css),
// não um <table> — trocado depois de confirmar ao vivo, testando com o
// usuário (inclusive mexendo direto pelo Console, fora do código da
// aplicação) que ESTE navegador simplesmente não reflui a largura desta
// tabela em particular (11 colunas, muitas linhas) depois do primeiro
// layout: nem <col>/<colgroup>, nem `width` no <th>, nem forçar reflow
// (offsetWidth, alternar table-layout fixed/auto), nem reconstruir a
// tabela inteira via innerHTML resolvia — uma tabela nova pequena com as
// mesmas classes CSS refluía normalmente, então não era o CSS; era algo
// específico do algoritmo de table-layout do navegador nesta tabela
// grande. CSS Grid usa um algoritmo de dimensionamento completamente
// diferente (grid-template-columns no container, sem nenhuma relação com
// o de <table>), então fica imune a esse problema.
//
// Pointer Events + setPointerCapture (trava mousemove/mouseup na própria
// alça, independente de onde o cursor esteja de verdade) + "click" com
// stopPropagation (a própria célula de cabeçalho é clicável pra ordenar,
// delegação em panelsEl mais abaixo — sem isso o clique de soltar o
// arrasto vazava pra cima da célula vizinha e disparava uma ordenação
// por engano). O arrasto em si só mostra uma linha-guia
// (.ips-col-resize-guide, position:fixed, puramente visual) seguindo o
// cursor — a largura só é aplicada de fato ao soltar, via renderPanels()
// (reconstrói o HTML com o `grid-template-columns` novo).
function wireColumnResize(root) {
root.querySelectorAll(".ips-col-resize-handle").forEach((handle) => {
handle.addEventListener("pointerdown", (event) => {
if (event.button !== undefined && event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
const headCell = handle.closest(".ips-grid-cell--head");
const wrap = handle.closest(".ips-table-scroll");
if (!headCell || !wrap) return;
const campo = handle.getAttribute("data-resize-campo");
const widthsState = handle.getAttribute("data-resize-target") === "auditoria" ? colWidthsAuditoria : colWidthsLinha;
const startX = event.clientX;
const startWidth = headCell.getBoundingClientRect().width;
const cellLeft = headCell.getBoundingClientRect().left;
const wrapRect = wrap.getBoundingClientRect();
let novaLargura = startWidth;
const guide = document.createElement("div");
guide.className = "ips-col-resize-guide";
guide.style.top = `${wrapRect.top}px`;
guide.style.height = `${wrapRect.height}px`;
document.body.appendChild(guide);
function posicionaGuide() {
guide.style.left = `${cellLeft + novaLargura}px`;
}
posicionaGuide();
document.body.classList.add("is-resizing-column");
handle.setPointerCapture(event.pointerId);
function onPointerMove(e) {
novaLargura = Math.max(PID_IPS_COL_MIN_WIDTH, Math.round(startWidth + (e.clientX - startX)));
posicionaGuide();
}
function onPointerUp() {
handle.removeEventListener("pointermove", onPointerMove);
handle.removeEventListener("pointerup", onPointerUp);
handle.removeEventListener("pointercancel", onPointerUp);
document.body.classList.remove("is-resizing-column");
guide.remove();
widthsState[campo] = novaLargura;
renderPanels();
}
handle.addEventListener("pointermove", onPointerMove);
handle.addEventListener("pointerup", onPointerUp);
handle.addEventListener("pointercancel", onPointerUp);
});
handle.addEventListener("click", (event) => event.stopPropagation());
});
}
// 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
? `<div class="ips-resumo">
<span><strong>${resumo.total_linhas}</strong> linhas no total</span>
<span><strong>${resumo.total_lancadas}</strong> com valor lançado</span>
<span><strong>${resumo.total_auditoria}</strong> em auditoria</span>
</div>`
: "";
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
? `<div class="ips-grid-cell" role="cell"><input type="text" class="ips-table-input" data-linha-campo="${campo}" value="${escapeAttr(linha[campo])}" /></div>`
: `<div class="ips-grid-cell" role="cell"><span class="ips-table-value">${escapeHtml(linha[campo])}</span></div>`;
})
.join("");
const removerHtml = concluida
? ""
: `<button type="button" class="icon-btn ips-remove-linha" data-remover-linha="${linha.id}" aria-label="Remover linha" title="Remover linha">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>`;
return `
<div class="ips-grid-row" role="row" data-linha-id="${linha.id}">
<div class="ips-grid-cell ips-table-actions" role="cell">${removerHtml}</div>
${celulasHtml}
</div>`;
})
.join("");
return `
${resumoHtml}
<div class="pa-table-wrap ips-table-scroll">
<div class="ips-grid" role="table" style="grid-template-columns:${gridTemplateColumns(campos, colWidthsLinha, 44)}">
<div class="ips-grid-row" role="row">
<div class="ips-grid-cell ips-grid-cell--head ips-table-actions" role="columnheader"></div>
${campos
.map(([campo, label]) => colHeaderHtml(campo, label, "linha", { key: linhaSortKey, dir: linhaSortDir }))
.join("")}
</div>
${
linhasHtml ||
`<div class="ips-grid-row pa-empty-row" role="row"><div class="ips-grid-cell" role="cell" style="grid-column:1 / -1">Nenhuma linha.</div></div>`
}
</div>
</div>
${concluida ? "" : `
<div class="ips-add-row">
<button type="button" class="btn-outline" data-ips-add-linha="${tipo}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14" stroke-linecap="round"/></svg>
Adicionar linha
</button>
</div>`}`;
}
// 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 `<p class="ips-empty">Nenhum item em auditoria — tudo foi lançado automaticamente.</p>`;
}
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 = `<span class="ips-empty-cell">—</span>`;
if (item.resolvida) {
acaoHtml = `<span class="ips-auditoria-resolvido" title="Vinculado a: ${escapeAttr(item.linha_vinculada_nome || "")}">Resolvido${
item.linha_vinculada_nome ? ` — ${item.linha_vinculada_nome}` : ""
}</span>`;
} else if (importacaoAtual.status !== "concluida" && PID_IPS_MOTIVOS_RESOLVIVEIS.includes(item.motivo)) {
acaoHtml = `<button type="button" class="btn-outline ips-vincular-btn" data-vincular-auditoria="${item.id}">Vincular pessoa</button>`;
}
return `
<div class="ips-grid-row" role="row">
<div class="ips-grid-cell ips-table-actions" role="cell">${acaoHtml}</div>
<div class="ips-grid-cell" role="cell">${item.nome || "—"}</div>
<div class="ips-grid-cell" role="cell">${item.cpf || "—"}</div>
<div class="ips-grid-cell" role="cell">${item.tipo === "T" ? "Titular" : "Dependente"}</div>
<div class="ips-grid-cell" role="cell">${PID_IPS_TIPO_LABELS[item.tipo_lancamento] || item.tipo_lancamento || "—"}</div>
<div class="ips-grid-cell" role="cell">${Number(item.valor).toFixed(2).replace(".", ",")}</div>
<div class="ips-grid-cell" role="cell"><span class="ips-auditoria-motivo">${PID_IPS_MOTIVO_LABELS[item.motivo] || item.motivo}</span></div>
<div class="ips-grid-cell" role="cell">${item.detalhe || ""}</div>
</div>`;
})
.join("");
return `
<div class="pa-table-wrap ips-table-scroll">
<div class="ips-grid" role="table" style="grid-template-columns:${gridTemplateColumns(campos, colWidthsAuditoria, PID_IPS_AUDITORIA_ACAO_WIDTH)}">
<div class="ips-grid-row" role="row">
<div class="ips-grid-cell ips-grid-cell--head ips-table-actions ips-col-sortable${
auditoriaSortKey === "acao" ? (auditoriaSortDir === 1 ? " is-sort-asc" : " is-sort-desc") : ""
}" role="columnheader" data-col-campo="acao" data-col-target="auditoria">Ação <span class="ips-sort-icon">↕</span></div>
${campos
.map(([campo, label]) => colHeaderHtml(campo, label, "auditoria", { key: auditoriaSortKey, dir: auditoriaSortDir }))
.join("")}
</div>
${linhasHtml}
</div>
</div>`;
}
// 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 `<p class="ips-empty">Nenhuma alteração registrada nesta importação ainda.</p>`;
}
const linhasHtml = alteracoes
.map((alt) => {
let detalheHtml;
if (alt.tipo === "edicao") {
const campoLabel = PID_IPS_CAMPO_LABELS[alt.campo] || alt.campo;
detalheHtml = `<strong>${escapeHtml(campoLabel)}:</strong> "${escapeHtml(alt.valor_anterior)}" → "${escapeHtml(alt.valor_novo)}"`;
} else if (alt.tipo === "inclusao") {
detalheHtml = "Linha incluída manualmente";
} else if (alt.tipo === "vinculo_automatico") {
detalheHtml = alt.vinculo_nome_destino
? `"${escapeHtml(alt.valor_novo)}" (arquivo da operadora) → <strong>${escapeHtml(alt.vinculo_nome_destino)}</strong> (planilha padrão)`
: `"${escapeHtml(alt.valor_novo)}" (vínculo já apagado)`;
} else {
detalheHtml = "Linha removida";
}
const acaoHtml = alt.revertida
? `<span class="ips-alteracao-revertida">Revertida</span>`
: importacaoAtual.status === "concluida"
? `<span class="ips-empty-cell">—</span>`
: `<button type="button" class="btn-outline ips-reverter-btn" data-reverter-alteracao="${alt.id}">${
alt.tipo === "vinculo_automatico" ? "Apagar vínculo" : "Reverter"
}</button>`;
return `
<tr>
<td class="ips-table-actions">${acaoHtml}</td>
<td>${new Date(alt.criado_em).toLocaleString("pt-BR")}</td>
<td><span class="ips-alteracao-tipo ips-alteracao-tipo--${alt.tipo}">${PID_IPS_ALTERACAO_TIPO_LABELS[alt.tipo] || alt.tipo}</span></td>
<td>${PID_IPS_TIPO_LABELS[alt.tipo_lancamento] || alt.tipo_lancamento || "—"}</td>
<td>${alt.linha_nome ? escapeHtml(alt.linha_nome) : "—"}</td>
<td>${detalheHtml}</td>
<td>${alt.usuario_nome ? escapeHtml(alt.usuario_nome) : "—"}</td>
</tr>`;
})
.join("");
return `
<div class="pa-table-wrap ips-table-scroll">
<table class="pa-table">
<thead>
<tr>
<th class="ips-table-actions">Ação</th>
<th>Quando</th>
<th>Tipo</th>
<th>Lançamento</th>
<th>Linha</th>
<th>Alteração</th>
<th>Usuário</th>
</tr>
</thead>
<tbody>${linhasHtml}</tbody>
</table>
</div>`;
}
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 `<div class="pa-tab-panel" data-ips-tab-panel="${key}" ${key === activeTab ? "" : "hidden"}>${html}</div>`;
})
.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 = `<p class="checklist-empty">Nenhuma linha em branco encontrada na planilha padrão.</p>`;
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 `
<label class="checklist-item">
<input type="radio" name="ips-vincular-linha" value="${l.id}" ${vincularLinhaSelecionada === l.id ? "checked" : ""} />
<span class="checklist-item__info">
<span class="checklist-item__nome">${nomePrincipal}</span>
<span class="checklist-item__departamento">${contexto}</span>
</span>
</label>`;
})
.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) {
// 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 (pidConfirm, não window.confirm — ver
// static/js/confirm-modal.js).
regracadModal.addEventListener("click", async (event) => {
if (event.target !== regracadModal) return;
if (await pidConfirm("Fechar o cadastro de regras sem salvar as alterações?", { perigoso: true })) {
fecharCadastroRegras();
}
});
}
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", () => {
// Enquanto travado por uma regra especial (ver aplicarTiposRegraEmpresa()),
// o checkbox fica disabled e este handler não dispara por clique do
// usuário — não precisa de exclusividade mútua explícita aqui.
custeioMensalidade.hidden = !formTipoMensalidade.checked;
});
}
if (formTipoCoparticipacao) {
formTipoCoparticipacao.addEventListener("change", () => {
custeioCoparticipacao.hidden = !formTipoCoparticipacao.checked;
});
}
if (formTipoRegraEmpresa) {
formTipoRegraEmpresa.addEventListener("change", () => {
if (regraEmpresaBox) regraEmpresaBox.hidden = !formTipoRegraEmpresa.checked;
if (!formTipoRegraEmpresa.checked) regraEmpresaSelecionada = null;
renderRegraEmpresaAtual();
// Enquanto nenhuma regra foi escolhida ainda (checkbox recém-marcado,
// regraEmpresaSelecionada ainda null), regraEmpresaTiposAtuais() é []
// e nada é travado — "Mensalidade"/"Coparticipação" continuam livres
// até o usuário de fato selecionar uma regra no picker.
aplicarTiposRegraEmpresa();
});
}
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();
aplicarTiposRegraEmpresa();
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 (
!(await pidConfirm(`Excluir a regra "${regracadRegraEmEdicao.nome}"? Essa ação não pode ser desfeita.`, {
perigoso: true,
}))
)
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");
let valorDigitado = input.value.trim();
if (PID_IPS_CAMPOS_EXPRESSAO_VALOR.has(campo)) {
const resultado = pidAvaliaExpressaoValorMonetario(valorDigitado);
if (resultado !== null) {
valorDigitado = resultado;
input.value = resultado; // reflete o resultado calculado na própria célula, mesmo antes do refetch abaixo
}
}
try {
await pidAtualizarLinhaPlanoSaude(linhaId, { [campo]: valorDigitado });
// Refaz o fetch da importação inteira (não só a linha editada) —
// o PATCH da linha não devolve o resumo por tipo nem a lista de
// alterações, então sem isso o contador "com valor lançado"/"em
// auditoria" e a aba Alterações continuavam mostrando o estado de
// antes da edição até o usuário sair e voltar pra essa importação
// (bug real reportado pelo usuário — a edição só aparecia depois
// de reabrir a tela do zero).
importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id);
reviewError.textContent = "";
renderTabs();
} 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 (!(await pidConfirm("Remover esta linha? Ela não vai aparecer no arquivo gerado.", { perigoso: true })))
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"));
const alteracao = (importacaoAtual.alteracoes || []).find((a) => a.id === alteracaoId);
const mensagemConfirmacao =
alteracao && alteracao.tipo === "vinculo_automatico"
? "Apagar este vínculo? O valor lançado nesta linha será zerado e esta divergência de nome voltará a cair em auditoria nas próximas importações."
: "Reverter esta alteração?";
if (!(await pidConfirm(mensagemConfirmacao))) 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 (!(await pidConfirm("Excluir esta importação do histórico? Essa ação não pode ser desfeita.", { perigoso: true })))
return;
try {
await pidExcluirImportacaoPlanoSaude(id);
selectedIds.delete(id);
await refreshList();
} catch (e) {
await pidAlert(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) {
await pidAlert(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 (!(await pidConfirm(confirmMsg, { perigoso: true }))) return;
listBulkDeleteBtn.disabled = true;
try {
await Promise.all(ids.map((id) => pidExcluirImportacaoPlanoSaude(id)));
selectedIds.clear();
} catch (e) {
await pidAlert(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 <th> 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 <th> 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 = `
<button type="button" class="ips-col-filter__btn" aria-label="Filtrar coluna" aria-expanded="false" title="Filtrar">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" stroke-linejoin="round"/></svg>
</button>
<div class="ips-col-filter__popup" hidden>
<input type="text" class="checklist-search" placeholder="Buscar..." />
<label class="modal-checkbox checklist-select-all"><input type="checkbox" checked /> Selecionar todos</label>
<div class="checklist-box"></div>
<div class="ips-col-filter__actions">
<button type="button" class="btn-ghost ips-col-filter__limpar">Limpar</button>
<button type="button" class="btn-solid ips-col-filter__aplicar">Aplicar</button>
</div>
</div>`;
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 valoresVisiveis() {
const termo = searchInput.value.trim().toLowerCase();
return valoresDistintos().filter((v) => labelValorFiltroLista(campo, v).toLowerCase().includes(termo));
}
function renderItens() {
const visiveis = valoresVisiveis();
box.innerHTML = visiveis.length
? visiveis
.map(
(v) =>
`<label class="checklist-item"><input type="checkbox" data-valor-filtro="${escapeAttr(v)}" ${marcados.has(v) ? "checked" : ""} /><span class="checklist-item__info">${escapeHtml(labelValorFiltroLista(campo, v))}</span></label>`
)
.join("")
: `<p class="checklist-empty">Nenhum valor encontrado.</p>`;
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);
// Enter no campo de busca restringe a seleção aos valores que batem com o
// termo digitado (igual marcar só eles manualmente) e já aplica — sem
// isso, digitar um código e teclar Enter só clicava em "Aplicar" com a
// seleção que já estava marcada (o popup abre com "Selecionar todos"
// ligado por padrão), então não filtrava nada na prática; era preciso
// desmarcar/marcar os checkboxes na mão antes de aplicar.
searchInput.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
event.preventDefault();
const visiveis = valoresVisiveis();
if (visiveis.length) marcados = new Set(visiveis);
aplicarBtn.click();
});
selectAllInput.addEventListener("change", () => {
const visiveis = valoresVisiveis();
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, atualizarBotaoAtivo });
}
["codigo_empresa", "nome_operadora", "status", "criado_por"].forEach(criarFiltroColuna);
const listResetBtn = document.getElementById("ips-list-reset-btn");
if (listResetBtn) {
listResetBtn.addEventListener("click", () => {
fecharTodosOsFiltrosColuna();
listSortKey = "criado_em";
listSortDir = -1;
listFiltros = { codigo_empresa: null, nome_operadora: null, status: null, criado_por: null };
filtrosColuna.forEach((f) => f.atualizarBotaoAtivo());
listPage = 1;
renderList();
});
}
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 (validadorArquivo.ehVazio()) {
formError.textContent = "Anexe ao menos um arquivo da operadora.";
return;
}
if (validadorArquivo.ehInvalido()) {
formError.textContent = "Corrija o(s) arquivo(s) da operadora (veja a mensagem abaixo de cada um) antes de processar.";
return;
}
formError.textContent = "";
const formData = new FormData();
if (origemQuestor) {
formData.append("competencia", competenciaIso);
} else {
formData.append("planilha_padrao", formPlanilha.files[0]);
}
validadorArquivo.arquivosSelecionados().forEach((file) => formData.append("arquivo_operadora", file));
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();
});