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