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

1517 lines
60 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",
};
const PID_IPS_CAMPOS_LINHA = [
["codigo_empresa", "Cód. Empresa", 90],
["nome_func", "Nome Funcionário", 200],
["cpf_func", "CPF Funcionário", 130],
["codigo_out_emp", "Cód. Out. Emp.", 90],
["data_inicial", "Data Inicial", 100],
["nome_dependente", "Nome Dependente", 200],
["cpf_dependente", "CPF Dependente", 130],
["valor_empresa", "Valor Empresa", 110],
["valor", "Valor", 90],
["descricao", "Descrição", 240],
];
const PID_IPS_CAMPOS_AUDITORIA = [
["nome", "Nome", 160],
["cpf", "CPF", 120],
["tipo", "Tipo", 100],
["tipo_lancamento", "Lançamento", 120],
["valor", "Valor", 100],
["motivo", "Motivo", 180],
["detalhe", "Justificativa", 320],
];
const PID_IPS_AUDITORIA_ACAO_WIDTH = 160;
const PID_IPS_COL_MIN_WIDTH = 60;
async function pidFetchImportacoesPlanoSaude() {
return (await pidApiRequest("/importacoes-plano-saude/")) || [];
}
async function pidFetchImportacaoPlanoSaude(id) {
return pidApiRequest(`/importacoes-plano-saude/${id}/`);
}
async function pidFetchOperadorasPlanoSaude() {
return (await pidApiRequest("/importacoes-plano-saude/operadoras/")) || [];
}
async function pidExcluirImportacaoPlanoSaude(id) {
return pidApiRequest(`/importacoes-plano-saude/${id}/`, { method: "DELETE" });
}
async function pidCriarImportacaoPlanoSaude(formData) {
return pidApiRequest("/importacoes-plano-saude/", { method: "POST", body: formData });
}
async function pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey) {
const formData = new FormData();
formData.append("tipo", tipo);
formData.append("arquivo", arquivo);
if (operadoraKey) formData.append("operadora", operadoraKey);
return pidApiRequest("/importacoes-plano-saude/validar-arquivo/", { method: "POST", body: formData });
}
async function pidAtualizarLinhaPlanoSaude(id, campos) {
return pidApiRequest(`/importacoes-plano-saude-linhas/${id}/`, { method: "PATCH", body: campos });
}
async function pidCriarLinhaPlanoSaude(importacaoId, tipoLancamento) {
return pidApiRequest("/importacoes-plano-saude-linhas/", {
method: "POST",
body: { importacao: importacaoId, tipo_lancamento: tipoLancamento },
});
}
async function pidExcluirLinhaPlanoSaude(id) {
return pidApiRequest(`/importacoes-plano-saude-linhas/${id}/`, { method: "DELETE" });
}
async function pidResolverAuditoriaPlanoSaude(itemId, linhaId) {
return pidApiRequest(`/importacoes-plano-saude-auditoria/${itemId}/resolver/`, {
method: "POST",
body: { linha_id: linhaId },
});
}
async function pidFetchRegrasCusteioPlanoSaude() {
return (await pidApiRequest("/regras-custeio-plano-saude/")) || [];
}
async function pidCriarRegraCusteioPlanoSaude(dados) {
return pidApiRequest("/regras-custeio-plano-saude/", { method: "POST", body: dados });
}
async function pidAtualizarRegraCusteioPlanoSaude(id, dados) {
return pidApiRequest(`/regras-custeio-plano-saude/${id}/`, { method: "PATCH", body: dados });
}
async function pidExcluirRegraCusteioPlanoSaude(id) {
return pidApiRequest(`/regras-custeio-plano-saude/${id}/`, { method: "DELETE" });
}
/**
* Resposta de /gerar/ é binária (CSV ou ZIP), não JSON — por isso não passa
* por pidApiRequest. Reaproveita pidEnsureCsrfCookie/pidGetCookie/pidErrorMessageFrom
* de api.js (globais na página) pra manter o mesmo tratamento de CSRF/erro.
*/
async function pidGerarArquivoPlanoSaude(id) {
await pidEnsureCsrfCookie();
const response = await fetch(`/api/importacoes-plano-saude/${id}/gerar/`, {
method: "POST",
credentials: "include",
headers: { "X-CSRFToken": pidGetCookie("csrftoken") || "" },
});
if (!response.ok) {
let mensagem = "Não foi possível gerar o arquivo.";
try {
mensagem = pidErrorMessageFrom(await response.json());
} catch (e) {
// resposta sem corpo JSON — mantém a mensagem genérica
}
throw new Error(mensagem);
}
const blob = await response.blob();
const disposition = response.headers.get("Content-Disposition") || "";
const match = disposition.match(/filename="?([^"]+)"?/);
const filename = match ? match[1] : "importacao_plano_saude.csv";
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
document.addEventListener("DOMContentLoaded", async () => {
const listView = document.getElementById("ips-list-view");
if (!listView) return;
const me = await pidGetMe();
if (!me) return;
const utilPerms = (me.permissoes_efetivas && me.permissoes_efetivas["utilitarios"]) || {};
const canUse = !!(utilPerms.apps && utilPerms.apps["importacao-plano-saude"]);
const noAccessEl = document.getElementById("ips-no-access");
const contentEl = document.getElementById("ips-content");
if (noAccessEl) noAccessEl.hidden = canUse;
if (contentEl) contentEl.hidden = !canUse;
if (!canUse) return;
const formView = document.getElementById("ips-form-view");
const reviewView = document.getElementById("ips-review-view");
const listBody = document.getElementById("ips-list-body");
const listEmpty = document.getElementById("ips-list-empty");
const listPagination = document.getElementById("ips-list-pagination");
const listCount = document.getElementById("ips-list-count");
const listSelectAll = document.getElementById("ips-list-select-all");
const listBulkActions = document.getElementById("ips-list-bulk-actions");
const listBulkCount = document.getElementById("ips-list-bulk-count");
const listBulkDeleteBtn = document.getElementById("ips-list-bulk-delete-btn");
const listSortHeaders = Array.from(document.querySelectorAll("#ips-list-table th[data-sort]"));
const newBtn = document.getElementById("ips-new-btn");
const formOperadora = document.getElementById("ips-form-operadora");
const formPlanilha = document.getElementById("ips-form-planilha");
const formPlanilhaName = document.getElementById("ips-form-planilha-name");
const formPlanilhaRemoveBtn = document.getElementById("ips-form-planilha-remove");
const formPlanilhaStatus = document.getElementById("ips-form-planilha-status");
const formArquivo = document.getElementById("ips-form-arquivo");
const formArquivoName = document.getElementById("ips-form-arquivo-name");
const formArquivoRemoveBtn = document.getElementById("ips-form-arquivo-remove");
const formArquivoStatus = document.getElementById("ips-form-arquivo-status");
const formTipoMensalidade = document.getElementById("ips-form-tipo-mensalidade");
const formTipoCoparticipacao = document.getElementById("ips-form-tipo-coparticipacao");
const custeioMensalidade = document.getElementById("ips-custeio-mensalidade");
const custeioCoparticipacao = document.getElementById("ips-custeio-coparticipacao");
const ipsRegraCombo = document.getElementById("ips-regra-combo");
const ipsRegraSearch = document.getElementById("ips-regra-search");
const ipsRegraComboList = document.getElementById("ips-regra-combo-list");
const ipsRegraAplicarBtn = document.getElementById("ips-regra-aplicar-btn");
const ipsRegraSalvarBtn = document.getElementById("ips-regra-salvar-btn");
const ipsRegraObservacoes = document.getElementById("ips-regra-observacoes");
const regrasGerenciarBtn = document.getElementById("ips-regras-gerenciar-btn");
const regrasModal = document.getElementById("ips-regras-modal");
const regrasLista = document.getElementById("ips-regras-lista");
const regrasError = document.getElementById("ips-regras-error");
const regrasFecharBtn = document.getElementById("ips-regras-fechar-btn");
const regraSaveModal = document.getElementById("ips-regra-save-modal");
const regraSaveTitle = document.getElementById("ips-regra-save-title");
const regraSaveNome = document.getElementById("ips-regra-save-nome");
const regraSaveObservacoes = document.getElementById("ips-regra-save-observacoes");
const regraSaveAtualizarField = document.getElementById("ips-regra-save-atualizar-field");
const regraSaveAtualizar = document.getElementById("ips-regra-save-atualizar");
const regraSaveAtualizarLabel = document.getElementById("ips-regra-save-atualizar-label");
const regraSaveError = document.getElementById("ips-regra-save-error");
const regraSaveCancelBtn = document.getElementById("ips-regra-save-cancel-btn");
const regraSaveConfirmBtn = document.getElementById("ips-regra-save-confirm-btn");
const formError = document.getElementById("ips-form-error");
const formSubmitBtn = document.getElementById("ips-form-submit-btn");
const formCancelBtn = document.getElementById("ips-form-cancel-btn");
const formBackBtn = document.getElementById("ips-form-back-btn");
const reviewSubtitle = document.getElementById("ips-review-subtitle");
const reviewError = document.getElementById("ips-review-error");
const reviewBackBtn = document.getElementById("ips-review-back-btn");
const gerarBtn = document.getElementById("ips-gerar-btn");
const tabsEl = document.getElementById("ips-tabs");
const panelsEl = document.getElementById("ips-tab-panels");
const vincularModal = document.getElementById("ips-vincular-modal");
const vincularResumo = document.getElementById("ips-vincular-resumo");
const vincularSearch = document.getElementById("ips-vincular-search");
const vincularLista = document.getElementById("ips-vincular-lista");
const vincularError = document.getElementById("ips-vincular-error");
const vincularCancelBtn = document.getElementById("ips-vincular-cancel-btn");
const vincularConfirmBtn = document.getElementById("ips-vincular-confirm-btn");
let importacoes = [];
let importacaoAtual = null;
let activeTab = null;
let listSortKey = "criado_em";
let listSortDir = -1;
let listPage = 1;
let selectedIds = new Set();
let currentPageIds = [];
let colWidthsLinha = {};
let colWidthsAuditoria = {};
// Ordenação por coluna das duas tabelas da revisão (clicar no cabeçalho) —
// mesma ideia de listSortKey/listSortDir do histórico, só que aqui é por
// painel (linha × auditoria) em vez de por tela inteira; também em
// memória, não persistida. As colunas em si têm ordem fixa (PID_IPS_CAMPOS_LINHA/
// PID_IPS_CAMPOS_AUDITORIA) — só as linhas são reordenáveis, por ordenação.
let linhaSortKey = null;
let linhaSortDir = 1;
let auditoriaSortKey = null;
let auditoriaSortDir = 1;
let vincularItemAtual = null;
let vincularLinhaSelecionada = null;
let operadorasCache = [];
// Banco de regras de custeio salvas (ver "Regra de custeio salva" no
// formulário) — regraAplicadaId rastreia qual regra está atualmente
// refletida no formulário (aplicada via select ou recém-salva), pra
// "Salvar regra atual..." nascer em modo "atualizar" por padrão nesse caso.
let regras = [];
let regraAplicadaId = null;
// Id atualmente escolhido no combobox pesquisável (#ips-regra-search) —
// diferente de regraAplicadaId: este só reflete o que está selecionado
// na caixa de busca; regraAplicadaId reflete o que está de fato refletido
// nos campos do formulário (pode ficar desatualizado em relação ao combo
// se o usuário digitar algo novo sem clicar em "Aplicar").
let regraSelecionadaId = null;
// Compara valores de uma célula da tabela de revisão pra ordenação por
// coluna — números no formato BR (milhar com ponto, decimal com vírgula,
// ex.: "1.234,56") são comparados numericamente; qualquer outra coisa
// (nome, CPF com hífen, data dd/mm/aaaa) cai pra comparação de texto.
function comparaValorRevisao(a, b) {
const paraNumero = (valor) => {
if (valor === null || valor === undefined || valor === "") return NaN;
const texto = String(valor).trim();
if (!/^-?[\d.,]+$/.test(texto)) return NaN;
return Number(texto.replace(/\./g, "").replace(",", "."));
};
const an = paraNumero(a);
const bn = paraNumero(b);
if (!Number.isNaN(an) && !Number.isNaN(bn)) return an - bn;
return String(a || "").localeCompare(String(b || ""), "pt-BR", { sensitivity: "base" });
}
/**
* Valida um único arquivo (planilha padrão OU arquivo da operadora) assim
* que o colaborador anexa, chamando /validar-arquivo/ — mesmo parser que
* seria usado no "Processar", só que sobre um upload descartável, pra
* apontar exatamente qual dos dois documentos está fora do padrão antes de
* precisar dos dois juntos. `ehInvalido()` (usado na validação do submit)
* só conta como inválido depois que a resposta chega — enquanto pendente
* ou sem arquivo, não bloqueia o envio (o create() no servidor valida de
* novo, é só essa checagem antecipada que não é garantida).
*/
function criarValidadorArquivo({ input, nameEl, removeBtn, statusEl, tipo, getOperadoraKey }) {
let estadoAtual = null;
function limpar() {
input.value = "";
nameEl.textContent = "Nenhum arquivo selecionado";
removeBtn.hidden = true;
statusEl.hidden = true;
statusEl.className = "ips-file-field__status";
statusEl.textContent = "";
estadoAtual = null;
}
async function validar() {
const arquivo = input.files[0];
if (!arquivo) {
limpar();
return;
}
nameEl.textContent = arquivo.name;
removeBtn.hidden = false;
statusEl.hidden = false;
statusEl.className = "ips-file-field__status ips-file-field__status--pendente";
statusEl.textContent = "Validando arquivo...";
estadoAtual = null;
try {
const operadoraKey = getOperadoraKey ? getOperadoraKey() : null;
const resultado = await pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey);
estadoAtual = resultado;
statusEl.className = `ips-file-field__status ${resultado.valido ? "ips-file-field__status--ok" : "ips-file-field__status--erro"}`;
statusEl.textContent = `${resultado.valido ? "✓" : "✗"} ${resultado.mensagem}`;
} catch (e) {
estadoAtual = { valido: false, mensagem: e.message };
statusEl.className = "ips-file-field__status ips-file-field__status--erro";
statusEl.textContent = `✗ ${e.message}`;
}
}
input.addEventListener("change", validar);
removeBtn.addEventListener("click", limpar);
return {
limpar,
revalidarSeAnexado: () => {
if (input.files[0]) validar();
},
ehInvalido: () => estadoAtual !== null && estadoAtual.valido === false,
};
}
const validadorPlanilha = criarValidadorArquivo({
input: formPlanilha,
nameEl: formPlanilhaName,
removeBtn: formPlanilhaRemoveBtn,
statusEl: formPlanilhaStatus,
tipo: "planilha",
});
const validadorArquivo = criarValidadorArquivo({
input: formArquivo,
nameEl: formArquivoName,
removeBtn: formArquivoRemoveBtn,
statusEl: formArquivoStatus,
tipo: "operadora",
getOperadoraKey: () => formOperadora.value,
});
function showView(view) {
listView.hidden = view !== "list";
formView.hidden = view !== "form";
reviewView.hidden = view !== "review";
}
function tiposLabel(tipos) {
return (tipos || []).map((t) => PID_IPS_TIPO_LABELS[t] || t).join(" + ");
}
const IPS_LIST_PAGE_SIZE = 10;
// Compara numericamente quando os dois valores parecem número (ex.:
// "218" vs "9" — string pura ordenaria "9" depois de "218"), senão cai pra
// comparação de texto (acentuada/case-insensitive) — cobre igual bem
// "Cód. Empresa" (numérico) e as colunas de texto/data ISO (que já ordenam
// certo por comparação de string).
function comparaValoresLista(a, b) {
const an = Number(a);
const bn = Number(b);
const ambosNumeros = a !== "" && b !== "" && !Number.isNaN(an) && !Number.isNaN(bn);
if (ambosNumeros) return an - bn;
return String(a).localeCompare(String(b), "pt-BR", { sensitivity: "base" });
}
const PID_IPS_LISTA_SORT_VALOR = {
codigo_empresa: (imp) => imp.codigo_empresa || "",
nome_operadora: (imp) => imp.nome_operadora || "",
tipos: (imp) => tiposLabel(imp.tipos_lancamento),
status: (imp) => imp.status || "",
criado_por: (imp) => imp.criado_por_nome || "",
criado_em: (imp) => imp.criado_em || "",
};
function renderListPagination(totalPaginas) {
if (totalPaginas <= 1) {
listPagination.innerHTML = "";
return;
}
listPagination.innerHTML = `
<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 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 = `
<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();
}
function resetForm() {
validadorPlanilha.limpar();
validadorArquivo.limpar();
formTipoMensalidade.checked = false;
formTipoCoparticipacao.checked = false;
custeioMensalidade.hidden = true;
custeioCoparticipacao.hidden = true;
custeioMensalidade.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false));
custeioCoparticipacao.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false));
PID_IPS_TIPOS.forEach((tipo) => {
PID_IPS_PESSOAS.forEach((pessoa) => {
const chave = `${tipo}-${pessoa}`;
const regraBox = document.getElementById(`ips-regra-especifica-${chave}`);
const limiteInput = document.getElementById(`ips-limite-${chave}`);
const percentualInput = document.getElementById(`ips-percentual-${chave}`);
if (regraBox) regraBox.hidden = true;
if (limiteInput) limiteInput.value = "";
if (percentualInput) percentualInput.value = "";
});
});
regraAplicadaId = null;
regraSelecionadaId = null;
if (ipsRegraSearch) ipsRegraSearch.value = "";
fecharRegraComboList();
mostrarObservacoesRegra(null);
formError.textContent = "";
}
// "150,00" / 150 / 150.5 -> "150,00"/"150,50" — usado pra repopular os
// inputs de limite/percentual (texto BR) a partir de uma regra salva, cujo
// custeio_por_tipo já vem validado/parseado como float pelo backend.
function formatarNumeroBr(valor) {
if (valor === null || valor === undefined || valor === "") return "";
const numero = Number(valor);
return Number.isNaN(numero) ? String(valor) : numero.toFixed(2).replace(".", ",");
}
/**
* Lê o custeio atualmente preenchido no formulário (quais tipos de
* lançamento estão marcados + o modo de custeio/limite/percentual de cada
* combinação tipo×pessoa) — mesmo formato de tipos_lancamento/custeio_por_tipo
* de RegraCusteioPlanoSaude, usado tanto pra montar o payload de "Salvar
* regra atual..." quanto (via os mesmos campos) pro submit da importação.
*/
function coletarCusteioAtual() {
const tipos_lancamento = [];
if (formTipoMensalidade.checked) tipos_lancamento.push("mensalidade");
if (formTipoCoparticipacao.checked) tipos_lancamento.push("coparticipacao");
const custeio_por_tipo = {};
PID_IPS_TIPOS.forEach((tipo) => {
custeio_por_tipo[tipo] = {};
PID_IPS_PESSOAS.forEach((pessoa) => {
const chave = `${tipo}-${pessoa}`;
const escolhido = document.querySelector(`input[name="ips-custeio-${chave}"]:checked`);
if (!escolhido) return;
const entrada = { modo: escolhido.value };
if (escolhido.value === "especifica") {
entrada.limite_valor = document.getElementById(`ips-limite-${chave}`).value.trim();
entrada.percentual = document.getElementById(`ips-percentual-${chave}`).value.trim();
}
custeio_por_tipo[tipo][pessoa] = entrada;
});
});
return { tipos_lancamento, custeio_por_tipo };
}
// Valida se o custeio dos tipos marcados está completo (titular e
// dependente de cada um) — reaproveitado pelo submit da importação e por
// "Salvar regra atual...", já que as duas ações exigem a mesma coisa:
// um custeio_por_tipo completo pros tipos selecionados.
function mensagemErroCusteio(tipos) {
if (!tipos.length) return "Selecione ao menos um tipo de importação.";
for (const tipo of tipos) {
for (const pessoa of PID_IPS_PESSOAS) {
const chave = `${tipo}-${pessoa}`;
const rotulo = `${PID_IPS_TIPO_LABELS[tipo]} (${PID_IPS_PESSOA_LABELS[pessoa]})`;
const escolhido = document.querySelector(`input[name="ips-custeio-${chave}"]:checked`);
if (!escolhido) return `Informe o custeio para ${rotulo}.`;
if (escolhido.value === "especifica") {
const limite = document.getElementById(`ips-limite-${chave}`).value.trim();
const percentual = document.getElementById(`ips-percentual-${chave}`).value.trim();
if (!limite && !percentual) return `Informe o limite de valor e/ou o percentual de custeio para ${rotulo}.`;
}
}
}
return null;
}
// Repõe no formulário uma regra salva (RegraCusteioPlanoSaude) — os campos
// continuam 100% editáveis depois, é só um preenchimento em massa, não um
// modo "somente leitura". `regra.custeio_por_tipo` já vem validado do
// backend (limite_valor/percentual como float, não texto BR).
function aplicarCusteio(regra) {
const tipos = Array.isArray(regra.tipos_lancamento) ? regra.tipos_lancamento : [];
formTipoMensalidade.checked = tipos.includes("mensalidade");
custeioMensalidade.hidden = !formTipoMensalidade.checked;
formTipoCoparticipacao.checked = tipos.includes("coparticipacao");
custeioCoparticipacao.hidden = !formTipoCoparticipacao.checked;
const custeioPorTipo = regra.custeio_por_tipo || {};
PID_IPS_TIPOS.forEach((tipo) => {
PID_IPS_PESSOAS.forEach((pessoa) => {
const chave = `${tipo}-${pessoa}`;
const regraBox = document.getElementById(`ips-regra-especifica-${chave}`);
const limiteInput = document.getElementById(`ips-limite-${chave}`);
const percentualInput = document.getElementById(`ips-percentual-${chave}`);
document.querySelectorAll(`input[name="ips-custeio-${chave}"]`).forEach((r) => (r.checked = false));
if (limiteInput) limiteInput.value = "";
if (percentualInput) percentualInput.value = "";
if (regraBox) regraBox.hidden = true;
const entrada = custeioPorTipo[tipo] && custeioPorTipo[tipo][pessoa];
if (!entrada || !entrada.modo) return;
const radio = document.querySelector(`input[name="ips-custeio-${chave}"][value="${entrada.modo}"]`);
if (radio) radio.checked = true;
if (entrada.modo === "especifica") {
if (limiteInput) limiteInput.value = formatarNumeroBr(entrada.limite_valor);
if (percentualInput) percentualInput.value = formatarNumeroBr(entrada.percentual);
if (regraBox) regraBox.hidden = false;
}
});
});
}
function operadoraLabel(key) {
const operadora = operadorasCache.find((op) => op.key === key);
return operadora ? operadora.label : key;
}
function mostrarObservacoesRegra(regra) {
if (regra && regra.observacoes) {
ipsRegraObservacoes.textContent = regra.observacoes;
ipsRegraObservacoes.hidden = false;
} else if (ipsRegraObservacoes) {
ipsRegraObservacoes.hidden = true;
ipsRegraObservacoes.textContent = "";
}
}
// Aplica a regra no formulário (custeio + operadora, se ela ainda existir
// no <select> de operadoras) e passa a rastreá-la como "a regra atual" —
// ver regraAplicadaId, usado por "Salvar regra atual..." pra saber se deve
// atualizar essa regra por padrão em vez de criar uma nova.
function aplicarRegraNoFormulario(regra) {
aplicarCusteio(regra);
if (regra.operadora && Array.from(formOperadora.options).some((o) => o.value === regra.operadora)) {
formOperadora.value = regra.operadora;
validadorArquivo.revalidarSeAnexado();
}
regraAplicadaId = regra.id;
regraSelecionadaId = regra.id;
if (ipsRegraSearch) ipsRegraSearch.value = regra.nome;
fecharRegraComboList();
mostrarObservacoesRegra(regra);
formError.textContent = "";
}
// Combobox pesquisável de "Regra de custeio salva" (#ips-regra-combo) —
// reaproveita o mesmo espírito de busca+lista já usado em outros lugares
// do app (ex.: "Vincular pessoa"), só que com o campo de busca também
// servindo de "display" do valor selecionado, no lugar do antigo <select>
// simples (deixou de escalar visualmente com o crescimento do banco de
// regras salvas).
function fecharRegraComboList() {
if (ipsRegraComboList) {
ipsRegraComboList.hidden = true;
ipsRegraComboList.innerHTML = "";
}
}
function renderRegraComboList(filtro) {
if (!ipsRegraComboList) return;
const termo = (filtro || "").trim().toLowerCase();
const filtradas = termo ? regras.filter((r) => r.nome.toLowerCase().includes(termo)) : regras;
if (!filtradas.length) {
ipsRegraComboList.innerHTML = `<p class="ips-regra-combo__empty">Nenhuma regra encontrada.</p>`;
} else {
ipsRegraComboList.innerHTML = filtradas
.map((r) => `<div class="ips-regra-combo__item" data-regra-id="${r.id}">${escapeHtml(r.nome)}</div>`)
.join("");
}
ipsRegraComboList.hidden = false;
}
async function refreshRegras() {
try {
regras = await pidFetchRegrasCusteioPlanoSaude();
} catch (e) {
regras = [];
}
// Se a regra selecionada no combobox foi excluída em outro lugar
// enquanto isso (ex.: pelo modal "Ver regras salvas"), limpa a seleção
// em vez de deixar o campo de busca mostrando um nome que não existe mais.
if (regraSelecionadaId !== null && !regras.some((r) => r.id === regraSelecionadaId)) {
regraSelecionadaId = null;
if (ipsRegraSearch) ipsRegraSearch.value = "";
}
}
function renderRegrasLista() {
if (!regrasLista) return;
if (!regras.length) {
regrasLista.innerHTML = `<p class="checklist-empty">Nenhuma regra salva ainda.</p>`;
return;
}
regrasLista.innerHTML = regras
.map((r) => {
const meta = `${r.operadora ? operadoraLabel(r.operadora) : "Qualquer operadora"} · ${tiposLabel(r.tipos_lancamento)}`;
return `
<div class="ips-regra-row">
<div class="ips-regra-row__info">
<p class="ips-regra-row__nome">${escapeHtml(r.nome)}</p>
<p class="ips-regra-row__meta">${escapeHtml(meta)}</p>
${r.observacoes ? `<p class="ips-regra-row__obs">${escapeHtml(r.observacoes)}</p>` : ""}
</div>
<div class="ips-regra-row__actions">
<button type="button" class="btn-outline" data-regra-aplicar="${r.id}">Aplicar</button>
<button type="button" class="icon-btn" data-regra-excluir="${r.id}" aria-label="Excluir regra" title="Excluir regra">
<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>
</div>
</div>`;
})
.join("");
}
function abrirRegraSaveModal() {
regraSaveError.textContent = "";
const regraBase = regras.find((r) => r.id === regraAplicadaId) || null;
regraSaveNome.value = regraBase ? regraBase.nome : "";
regraSaveObservacoes.value = regraBase ? regraBase.observacoes || "" : "";
if (regraBase) {
regraSaveAtualizarField.hidden = false;
regraSaveAtualizar.checked = true;
regraSaveAtualizarLabel.textContent = `Atualizar a regra selecionada ("${regraBase.nome}")`;
} else {
regraSaveAtualizarField.hidden = true;
regraSaveAtualizar.checked = false;
}
regraSaveTitle.textContent = regraBase ? "Salvar regra de custeio" : "Salvar nova regra de custeio";
regraSaveModal.hidden = false;
regraSaveNome.focus();
}
async function openNewForm() {
resetForm();
try {
operadorasCache = await pidFetchOperadorasPlanoSaude();
formOperadora.innerHTML =
`<option value="" disabled selected>Selecionar operadora</option>` +
operadorasCache.map((op) => `<option value="${op.key}">${op.label}</option>`).join("");
} catch (e) {
operadorasCache = [];
formOperadora.innerHTML = "";
}
await refreshRegras();
showView("form");
}
function abrirRevisao(importacao) {
importacaoAtual = importacao;
activeTab = null;
reviewSubtitle.textContent = `${importacao.nome_operadora} — criada em ${new Date(importacao.criado_em).toLocaleString("pt-BR")}`;
reviewError.textContent = "";
renderTabs();
showView("review");
}
function renderTabs() {
const tipos = importacaoAtual.tipos_lancamento || [];
const tabKeys = [...tipos, "auditoria"];
if (!activeTab || !tabKeys.includes(activeTab)) activeTab = tabKeys[0];
tabsEl.innerHTML = tabKeys
.map((key) => {
const label = key === "auditoria" ? "Auditoria" : PID_IPS_TIPO_LABELS[key] || key;
return `<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 colGroupHtml(campos, widths, extraColWidth) {
const extraCol = extraColWidth ? `<col style="width:${extraColWidth}px">` : "";
const cols = campos.map(([campo, , largura]) => `<col style="width:${widths[campo] || largura}px">`).join("");
return `<colgroup>${extraCol}${cols}</colgroup>`;
}
function resizeHandleHtml(campo, target) {
return `<span class="ips-col-resize-handle" data-resize-campo="${campo}" data-resize-target="${target}"></span>`;
}
// Colunas têm ordem fixa (não são mais arrastáveis) — só o clique pra
// ordenar as linhas por essa coluna continua disponível.
function colHeaderHtml(campo, label, target, sortState) {
const ordenadaPorMim = sortState && sortState.key === campo;
const sortClasse = ordenadaPorMim ? (sortState.dir === 1 ? " is-sort-asc" : " is-sort-desc") : "";
return `<th class="ips-col-sortable${sortClasse}" data-col-campo="${campo}" data-col-target="${target}">${label} <span class="ips-sort-icon">↕</span>${resizeHandleHtml(campo, target)}</th>`;
}
function wireColumnResize(root) {
root.querySelectorAll(".ips-col-resize-handle").forEach((handle) => {
handle.addEventListener("mousedown", (event) => {
event.preventDefault();
const th = handle.closest("th");
const table = th.closest("table");
const colIndex = Array.from(th.parentElement.children).indexOf(th);
const col = table.querySelectorAll("col")[colIndex];
if (!col) return;
const campo = handle.getAttribute("data-resize-campo");
const widthsState = handle.getAttribute("data-resize-target") === "auditoria" ? colWidthsAuditoria : colWidthsLinha;
const startX = event.clientX;
const startWidth = th.getBoundingClientRect().width;
document.body.classList.add("is-resizing-column");
function onMouseMove(e) {
const novaLargura = Math.max(PID_IPS_COL_MIN_WIDTH, Math.round(startWidth + (e.clientX - startX)));
col.style.width = `${novaLargura}px`;
}
function onMouseUp() {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
document.body.classList.remove("is-resizing-column");
widthsState[campo] = parseInt(col.style.width, 10);
}
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
});
}
function panelHtmlParaTipo(tipo) {
const campos = PID_IPS_CAMPOS_LINHA;
let linhas = (importacaoAtual.linhas || []).filter((l) => l.tipo_lancamento === tipo);
if (linhaSortKey) {
linhas = [...linhas].sort((a, b) => comparaValorRevisao(a[linhaSortKey], b[linhaSortKey]) * linhaSortDir);
}
const resumo = (importacaoAtual.resumo_por_tipo || []).find((r) => r.tipo_lancamento === tipo);
const resumoHtml = resumo
? `<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) => `
<tr data-linha-id="${linha.id}">
<td class="ips-table-actions">
<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>
</td>
${campos
.map(
([campo]) =>
`<td><input type="text" class="ips-table-input" data-linha-campo="${campo}" value="${escapeAttr(linha[campo])}" /></td>`
)
.join("")}
</tr>`
)
.join("");
return `
${resumoHtml}
<div class="pa-table-wrap ips-table-scroll">
<table class="pa-table">
${colGroupHtml(campos, colWidthsLinha, 44)}
<thead>
<tr><th class="ips-table-actions"></th>${campos
.map(([campo, label]) => colHeaderHtml(campo, label, "linha", { key: linhaSortKey, dir: linhaSortDir }))
.join("")}</tr>
</thead>
<tbody>${linhasHtml || `<tr class="pa-empty-row"><td colspan="${campos.length + 1}">Nenhuma linha.</td></tr>`}</tbody>
</table>
</div>
<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>`;
}
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) {
itens = [...itens].sort((a, b) => comparaValorRevisao(a[auditoriaSortKey], b[auditoriaSortKey]) * 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 (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 `
<tr>
<td class="ips-table-actions">${acaoHtml}</td>
<td>${item.nome || "—"}</td>
<td>${item.cpf || "—"}</td>
<td>${item.tipo === "T" ? "Titular" : "Dependente"}</td>
<td>${PID_IPS_TIPO_LABELS[item.tipo_lancamento] || item.tipo_lancamento || "—"}</td>
<td>${Number(item.valor).toFixed(2).replace(".", ",")}</td>
<td><span class="ips-auditoria-motivo">${PID_IPS_MOTIVO_LABELS[item.motivo] || item.motivo}</span></td>
<td>${item.detalhe || ""}</td>
</tr>`;
})
.join("");
return `
<div class="pa-table-wrap ips-table-scroll">
<table class="pa-table">
${colGroupHtml(campos, colWidthsAuditoria, PID_IPS_AUDITORIA_ACAO_WIDTH)}
<thead>
<tr><th class="ips-table-actions">Ação</th>${campos
.map(([campo, label]) => colHeaderHtml(campo, label, "auditoria", { key: auditoriaSortKey, dir: auditoriaSortDir }))
.join("")}</tr>
</thead>
<tbody>${linhasHtml}</tbody>
</table>
</div>`;
}
function renderPanels() {
const tipos = importacaoAtual.tipos_lancamento || [];
panelsEl.innerHTML = [...tipos, "auditoria"]
.map((key) => {
const html = key === "auditoria" ? panelHtmlAuditoria() : 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 (formOperadora) {
formOperadora.addEventListener("change", () => validadorArquivo.revalidarSeAnexado());
}
if (formTipoMensalidade) {
formTipoMensalidade.addEventListener("change", () => {
custeioMensalidade.hidden = !formTipoMensalidade.checked;
});
}
if (formTipoCoparticipacao) {
formTipoCoparticipacao.addEventListener("change", () => {
custeioCoparticipacao.hidden = !formTipoCoparticipacao.checked;
});
}
if (ipsRegraSearch) {
ipsRegraSearch.addEventListener("focus", () => renderRegraComboList(ipsRegraSearch.value));
ipsRegraSearch.addEventListener("input", () => {
// Digitar de novo invalida a seleção anterior — só volta a valer
// depois de clicar numa regra da lista (evita "Aplicar" usar uma
// regra antiga enquanto o texto mostrado já é outra busca).
regraSelecionadaId = null;
renderRegraComboList(ipsRegraSearch.value);
});
}
if (ipsRegraComboList) {
ipsRegraComboList.addEventListener("click", (event) => {
const item = event.target.closest("[data-regra-id]");
if (!item) return;
const id = Number(item.getAttribute("data-regra-id"));
const regra = regras.find((r) => r.id === id);
if (!regra) return;
regraSelecionadaId = regra.id;
ipsRegraSearch.value = regra.nome;
fecharRegraComboList();
mostrarObservacoesRegra(regra);
});
}
document.addEventListener("click", (event) => {
if (ipsRegraCombo && !ipsRegraCombo.contains(event.target)) fecharRegraComboList();
});
if (ipsRegraAplicarBtn) {
ipsRegraAplicarBtn.addEventListener("click", () => {
if (!regraSelecionadaId) {
formError.textContent = "Selecione uma regra salva pra aplicar.";
return;
}
const regra = regras.find((r) => r.id === regraSelecionadaId);
if (regra) aplicarRegraNoFormulario(regra);
});
}
if (ipsRegraSalvarBtn) {
ipsRegraSalvarBtn.addEventListener("click", () => {
const { tipos_lancamento } = coletarCusteioAtual();
const erro = mensagemErroCusteio(tipos_lancamento);
if (erro) {
formError.textContent = erro;
return;
}
formError.textContent = "";
abrirRegraSaveModal();
});
}
if (regraSaveCancelBtn) regraSaveCancelBtn.addEventListener("click", () => (regraSaveModal.hidden = true));
if (regraSaveModal) {
regraSaveModal.addEventListener("click", (event) => {
if (event.target === regraSaveModal) regraSaveModal.hidden = true;
});
}
if (regraSaveConfirmBtn) {
regraSaveConfirmBtn.addEventListener("click", async () => {
const nome = regraSaveNome.value.trim();
if (!nome) {
regraSaveError.textContent = "Informe um nome para a regra.";
return;
}
const { tipos_lancamento, custeio_por_tipo } = coletarCusteioAtual();
const payload = {
nome,
operadora: formOperadora.value || "",
tipos_lancamento,
custeio_por_tipo,
observacoes: regraSaveObservacoes.value.trim(),
};
const atualizando = !regraSaveAtualizarField.hidden && regraSaveAtualizar.checked && regraAplicadaId;
regraSaveConfirmBtn.disabled = true;
try {
const regraSalva = atualizando
? await pidAtualizarRegraCusteioPlanoSaude(regraAplicadaId, payload)
: await pidCriarRegraCusteioPlanoSaude(payload);
await refreshRegras();
regraAplicadaId = regraSalva.id;
regraSelecionadaId = regraSalva.id;
if (ipsRegraSearch) ipsRegraSearch.value = regraSalva.nome;
mostrarObservacoesRegra(regraSalva);
regraSaveModal.hidden = true;
} catch (e) {
regraSaveError.textContent = e.message;
} finally {
regraSaveConfirmBtn.disabled = false;
}
});
}
if (regrasGerenciarBtn) {
regrasGerenciarBtn.addEventListener("click", async () => {
regrasError.textContent = "";
await refreshRegras();
renderRegrasLista();
regrasModal.hidden = false;
});
}
if (regrasFecharBtn) regrasFecharBtn.addEventListener("click", () => (regrasModal.hidden = true));
if (regrasModal) {
regrasModal.addEventListener("click", (event) => {
if (event.target === regrasModal) regrasModal.hidden = true;
});
}
if (regrasLista) {
regrasLista.addEventListener("click", async (event) => {
const aplicarBtn = event.target.closest("[data-regra-aplicar]");
if (aplicarBtn) {
const id = Number(aplicarBtn.getAttribute("data-regra-aplicar"));
const regra = regras.find((r) => r.id === id);
if (regra) {
aplicarRegraNoFormulario(regra);
regrasModal.hidden = true;
}
return;
}
const excluirBtn = event.target.closest("[data-regra-excluir]");
if (excluirBtn) {
const id = Number(excluirBtn.getAttribute("data-regra-excluir"));
const regra = regras.find((r) => r.id === id);
if (!regra) return;
if (!window.confirm(`Excluir a regra "${regra.nome}"? Essa ação não pode ser desfeita.`)) return;
excluirBtn.disabled = true;
try {
await pidExcluirRegraCusteioPlanoSaude(id);
if (regraAplicadaId === id) regraAplicadaId = null;
await refreshRegras();
renderRegrasLista();
regrasError.textContent = "";
} catch (e) {
regrasError.textContent = e.message;
excluirBtn.disabled = false;
}
}
});
}
PID_IPS_TIPOS.forEach((tipo) => {
const custeioBox = document.getElementById(`ips-custeio-${tipo}`);
if (!custeioBox) return;
custeioBox.addEventListener("change", (event) => {
const pessoa = PID_IPS_PESSOAS.find((p) => event.target.name === `ips-custeio-${tipo}-${p}`);
if (!pessoa) return;
const regraBox = document.getElementById(`ips-regra-especifica-${tipo}-${pessoa}`);
if (regraBox) regraBox.hidden = event.target.value !== "especifica";
});
});
if (tabsEl) {
tabsEl.addEventListener("click", (event) => {
const btn = event.target.closest("[data-ips-tab]");
if (!btn) return;
activeTab = btn.getAttribute("data-ips-tab");
renderTabs();
});
}
if (panelsEl) {
panelsEl.addEventListener("change", async (event) => {
const input = event.target.closest("[data-linha-campo]");
if (!input) return;
const row = input.closest("[data-linha-id]");
const linhaId = Number(row.getAttribute("data-linha-id"));
const campo = input.getAttribute("data-linha-campo");
try {
await pidAtualizarLinhaPlanoSaude(linhaId, { [campo]: input.value.trim() });
reviewError.textContent = "";
} catch (e) {
reviewError.textContent = e.message;
}
});
panelsEl.addEventListener("click", async (event) => {
if (!event.target.closest(".ips-col-resize-handle")) {
const sortTh = event.target.closest("[data-col-campo]");
if (sortTh) {
const campo = sortTh.getAttribute("data-col-campo");
if (sortTh.getAttribute("data-col-target") === "auditoria") {
if (auditoriaSortKey === campo) auditoriaSortDir *= -1;
else {
auditoriaSortKey = campo;
auditoriaSortDir = 1;
}
} else {
if (linhaSortKey === campo) linhaSortDir *= -1;
else {
linhaSortKey = campo;
linhaSortDir = 1;
}
}
renderPanels();
return;
}
}
const addBtn = event.target.closest("[data-ips-add-linha]");
if (addBtn) {
const tipo = addBtn.getAttribute("data-ips-add-linha");
addBtn.disabled = true;
try {
const novaLinha = await pidCriarLinhaPlanoSaude(importacaoAtual.id, tipo);
importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id);
reviewError.textContent = "";
renderTabs();
const novaRow = panelsEl.querySelector(`[data-linha-id="${novaLinha.id}"]`);
const primeiroInput = novaRow && novaRow.querySelector("[data-linha-campo]");
if (primeiroInput) {
primeiroInput.scrollIntoView({ block: "nearest" });
primeiroInput.focus();
}
} catch (e) {
reviewError.textContent = e.message;
addBtn.disabled = false;
}
return;
}
const removeBtn = event.target.closest("[data-remover-linha]");
if (removeBtn) {
const linhaId = Number(removeBtn.getAttribute("data-remover-linha"));
if (!window.confirm("Remover esta linha? Ela não vai aparecer no arquivo gerado.")) return;
removeBtn.disabled = true;
try {
await pidExcluirLinhaPlanoSaude(linhaId);
importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id);
reviewError.textContent = "";
renderTabs();
} catch (e) {
reviewError.textContent = e.message;
removeBtn.disabled = false;
}
return;
}
const vincularBtn = event.target.closest("[data-vincular-auditoria]");
if (vincularBtn) {
const itemId = Number(vincularBtn.getAttribute("data-vincular-auditoria"));
const item = (importacaoAtual.itens_auditoria || []).find((i) => i.id === itemId);
if (item) abrirVincularModal(item);
}
});
}
if (gerarBtn) {
gerarBtn.addEventListener("click", async () => {
reviewError.textContent = "";
gerarBtn.disabled = true;
try {
await pidGerarArquivoPlanoSaude(importacaoAtual.id);
} catch (e) {
reviewError.textContent = e.message;
} finally {
gerarBtn.disabled = false;
}
});
}
if (listBody) {
listBody.addEventListener("click", async (event) => {
const removeBtn = event.target.closest("[data-remover-importacao]");
if (removeBtn) {
const id = Number(removeBtn.getAttribute("data-remover-importacao"));
if (!window.confirm("Excluir esta importação do histórico? Essa ação não pode ser desfeita.")) return;
try {
await pidExcluirImportacaoPlanoSaude(id);
selectedIds.delete(id);
await refreshList();
} catch (e) {
window.alert(e.message);
}
return;
}
if (event.target.closest("[data-select-id]")) return;
const row = event.target.closest("[data-importacao-id]");
if (!row) return;
const id = Number(row.getAttribute("data-importacao-id"));
try {
const detalhe = await pidFetchImportacaoPlanoSaude(id);
abrirRevisao(detalhe);
} catch (e) {
window.alert(e.message);
}
});
listBody.addEventListener("change", (event) => {
const checkbox = event.target.closest("[data-select-id]");
if (!checkbox) return;
const id = Number(checkbox.getAttribute("data-select-id"));
if (checkbox.checked) selectedIds.add(id);
else selectedIds.delete(id);
updateSelectAllState();
updateBulkActionsUI();
});
}
if (listSelectAll) {
listSelectAll.addEventListener("change", () => {
if (listSelectAll.checked) {
currentPageIds.forEach((id) => selectedIds.add(id));
} else {
currentPageIds.forEach((id) => selectedIds.delete(id));
}
renderList();
});
}
if (listBulkDeleteBtn) {
listBulkDeleteBtn.addEventListener("click", async () => {
const ids = Array.from(selectedIds);
if (!ids.length) return;
const confirmMsg =
ids.length === 1
? "Excluir esta importação do histórico? Essa ação não pode ser desfeita."
: `Excluir estas ${ids.length} importações do histórico? Essa ação não pode ser desfeita.`;
if (!window.confirm(confirmMsg)) return;
listBulkDeleteBtn.disabled = true;
try {
await Promise.all(ids.map((id) => pidExcluirImportacaoPlanoSaude(id)));
selectedIds.clear();
} catch (e) {
window.alert(e.message);
} finally {
listBulkDeleteBtn.disabled = false;
await refreshList();
}
});
}
listSortHeaders.forEach((th) => {
th.addEventListener("click", () => {
const key = th.getAttribute("data-sort");
if (listSortKey === key) {
listSortDir *= -1;
} else {
listSortKey = key;
listSortDir = 1;
}
listPage = 1;
renderList();
});
});
if (listPagination) {
listPagination.addEventListener("click", (event) => {
if (event.target.closest("#ips-list-prev-btn")) {
listPage -= 1;
renderList();
} else if (event.target.closest("#ips-list-next-btn")) {
listPage += 1;
renderList();
}
});
}
if (formSubmitBtn) {
formSubmitBtn.addEventListener("click", async () => {
const { tipos_lancamento: tipos, custeio_por_tipo: custeio } = coletarCusteioAtual();
if (!formOperadora.value) {
formError.textContent = "Selecione a operadora.";
return;
}
if (!formPlanilha.files[0]) {
formError.textContent = "Anexe a planilha padrão do Questor.";
return;
}
if (!formArquivo.files[0]) {
formError.textContent = "Anexe o arquivo da operadora.";
return;
}
if (validadorPlanilha.ehInvalido() || validadorArquivo.ehInvalido()) {
formError.textContent = "Corrija os arquivos anexados (veja a mensagem abaixo de cada um) antes de processar.";
return;
}
const erroCusteio = mensagemErroCusteio(tipos);
if (erroCusteio) {
formError.textContent = erroCusteio;
return;
}
formError.textContent = "";
const formData = new FormData();
formData.append("operadora", formOperadora.value);
formData.append("planilha_padrao", formPlanilha.files[0]);
formData.append("arquivo_operadora", formArquivo.files[0]);
formData.append("tipos_lancamento", tipos.join(","));
tipos.forEach((tipo) => {
PID_IPS_PESSOAS.forEach((pessoa) => {
const entrada = custeio[tipo][pessoa];
formData.append(`custeio_${tipo}_${pessoa}`, entrada.modo);
if (entrada.modo === "especifica") {
formData.append(`limite_valor_${tipo}_${pessoa}`, entrada.limite_valor || "");
formData.append(`percentual_${tipo}_${pessoa}`, entrada.percentual || "");
}
});
});
formSubmitBtn.disabled = true;
formSubmitBtn.textContent = "Processando...";
try {
const importacao = await pidCriarImportacaoPlanoSaude(formData);
abrirRevisao(importacao);
await refreshList();
} catch (e) {
formError.textContent = e.message;
} finally {
formSubmitBtn.disabled = false;
formSubmitBtn.textContent = "Processar";
}
});
}
await refreshList();
});