1541 lines
61 KiB
JavaScript
1541 lines
61 KiB
JavaScript
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 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 = `
|
||
<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();
|
||
}
|
||
|
||
// 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 <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, """);
|
||
}
|
||
|
||
// Igual a escapeAttr, mas também escapa "<"/">"/"&" — usado pra inserir via
|
||
// innerHTML texto livre digitado pelo colaborador (nome/observações de uma
|
||
// regra de custeio salva), que ao contrário do resto dos campos desta tela
|
||
// (nomes de operadora/arquivo, vindos de fontes fixas) pode conter
|
||
// qualquer caractere.
|
||
function escapeHtml(value) {
|
||
return (value === null || value === undefined ? "" : String(value)).replace(
|
||
/[&<>"]/g,
|
||
(ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]
|
||
);
|
||
}
|
||
|
||
function colGroupHtml(campos, widths, extraColWidth) {
|
||
const extraCol = extraColWidth ? `<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 (ipsRegraLimparBtn) {
|
||
ipsRegraLimparBtn.addEventListener("click", () => {
|
||
limparRegraSelecionada();
|
||
limparCusteioForm();
|
||
formError.textContent = "";
|
||
});
|
||
}
|
||
|
||
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();
|
||
});
|