2214 lines
92 KiB
JavaScript
2214 lines
92 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],
|
||
];
|
||
|
||
// Lookup campo -> label pra aba "Alterações" (ex.: "valor" -> "Valor"),
|
||
// derivado de PID_IPS_CAMPOS_LINHA pra nunca divergir do rótulo já usado na
|
||
// própria coluna da tabela de Mensalidade/Coparticipação.
|
||
const PID_IPS_CAMPO_LABELS = Object.fromEntries(PID_IPS_CAMPOS_LINHA.map(([campo, label]) => [campo, label]));
|
||
|
||
// Espelha ImportacaoPlanoSaudeAlteracao.TIPO_CHOICES (models.py).
|
||
const PID_IPS_ALTERACAO_TIPO_LABELS = {
|
||
edicao: "Edição",
|
||
inclusao: "Inclusão",
|
||
exclusao: "Exclusão",
|
||
};
|
||
|
||
const PID_IPS_CAMPOS_AUDITORIA = [
|
||
["nome", "Nome", 160],
|
||
["cpf", "CPF", 120],
|
||
["tipo", "Tipo", 100],
|
||
["tipo_lancamento", "Lançamento", 120],
|
||
["valor", "Valor", 100],
|
||
["motivo", "Motivo", 180],
|
||
["detalhe", "Justificativa", 320],
|
||
];
|
||
|
||
const PID_IPS_AUDITORIA_ACAO_WIDTH = 160;
|
||
|
||
const PID_IPS_COL_MIN_WIDTH = 60;
|
||
|
||
async function pidFetchImportacoesPlanoSaude() {
|
||
return (await pidApiRequest("/importacoes-plano-saude/")) || [];
|
||
}
|
||
|
||
async function pidFetchImportacaoPlanoSaude(id) {
|
||
return pidApiRequest(`/importacoes-plano-saude/${id}/`);
|
||
}
|
||
|
||
async function pidFetchOperadorasPlanoSaude() {
|
||
return (await pidApiRequest("/importacoes-plano-saude/operadoras/")) || [];
|
||
}
|
||
|
||
async function pidFetchRegrasEmpresaPlanoSaude() {
|
||
return (await pidApiRequest("/importacoes-plano-saude/regras-empresa/")) || [];
|
||
}
|
||
|
||
async function pidExcluirImportacaoPlanoSaude(id) {
|
||
return pidApiRequest(`/importacoes-plano-saude/${id}/`, { method: "DELETE" });
|
||
}
|
||
|
||
async function pidCriarImportacaoPlanoSaude(formData) {
|
||
return pidApiRequest("/importacoes-plano-saude/", { method: "POST", body: formData });
|
||
}
|
||
|
||
async function pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey) {
|
||
const formData = new FormData();
|
||
formData.append("tipo", tipo);
|
||
formData.append("arquivo", arquivo);
|
||
if (operadoraKey) formData.append("operadora", operadoraKey);
|
||
return pidApiRequest("/importacoes-plano-saude/validar-arquivo/", { method: "POST", body: formData });
|
||
}
|
||
|
||
async function pidAtualizarLinhaPlanoSaude(id, campos) {
|
||
return pidApiRequest(`/importacoes-plano-saude-linhas/${id}/`, { method: "PATCH", body: campos });
|
||
}
|
||
|
||
async function pidCriarLinhaPlanoSaude(importacaoId, tipoLancamento) {
|
||
return pidApiRequest("/importacoes-plano-saude-linhas/", {
|
||
method: "POST",
|
||
body: { importacao: importacaoId, tipo_lancamento: tipoLancamento },
|
||
});
|
||
}
|
||
|
||
async function pidExcluirLinhaPlanoSaude(id) {
|
||
return pidApiRequest(`/importacoes-plano-saude-linhas/${id}/`, { method: "DELETE" });
|
||
}
|
||
|
||
async function pidResolverAuditoriaPlanoSaude(itemId, linhaId) {
|
||
return pidApiRequest(`/importacoes-plano-saude-auditoria/${itemId}/resolver/`, {
|
||
method: "POST",
|
||
body: { linha_id: linhaId },
|
||
});
|
||
}
|
||
|
||
async function pidReverterAlteracaoPlanoSaude(id) {
|
||
return pidApiRequest(`/importacoes-plano-saude-alteracoes/${id}/reverter/`, { method: "POST" });
|
||
}
|
||
|
||
async function pidFetchRegrasCusteioPlanoSaude() {
|
||
return (await pidApiRequest("/regras-custeio-plano-saude/")) || [];
|
||
}
|
||
|
||
async function pidCriarRegraCusteioPlanoSaude(dados) {
|
||
return pidApiRequest("/regras-custeio-plano-saude/", { method: "POST", body: dados });
|
||
}
|
||
|
||
async function pidAtualizarRegraCusteioPlanoSaude(id, dados) {
|
||
return pidApiRequest(`/regras-custeio-plano-saude/${id}/`, { method: "PATCH", body: dados });
|
||
}
|
||
|
||
async function pidExcluirRegraCusteioPlanoSaude(id) {
|
||
return pidApiRequest(`/regras-custeio-plano-saude/${id}/`, { method: "DELETE" });
|
||
}
|
||
|
||
async function pidBuscarNomeEmpresaPlanoSaude(codigoEmpresa) {
|
||
return pidApiRequest(`/regras-custeio-plano-saude/nome-empresa/?codigo_empresa=${encodeURIComponent(codigoEmpresa)}`);
|
||
}
|
||
|
||
/**
|
||
* Resposta de /gerar/ é binária (CSV ou ZIP), não JSON — por isso não passa
|
||
* por pidApiRequest. Reaproveita pidEnsureCsrfCookie/pidGetCookie/pidErrorMessageFrom
|
||
* de api.js (globais na página) pra manter o mesmo tratamento de CSRF/erro.
|
||
*/
|
||
async function pidGerarArquivoPlanoSaude(id) {
|
||
await pidEnsureCsrfCookie();
|
||
const response = await fetch(`/api/importacoes-plano-saude/${id}/gerar/`, {
|
||
method: "POST",
|
||
credentials: "include",
|
||
headers: { "X-CSRFToken": pidGetCookie("csrftoken") || "" },
|
||
});
|
||
|
||
if (!response.ok) {
|
||
let mensagem = "Não foi possível gerar o arquivo.";
|
||
try {
|
||
mensagem = pidErrorMessageFrom(await response.json());
|
||
} catch (e) {
|
||
// resposta sem corpo JSON — mantém a mensagem genérica
|
||
}
|
||
throw new Error(mensagem);
|
||
}
|
||
|
||
const blob = await response.blob();
|
||
const disposition = response.headers.get("Content-Disposition") || "";
|
||
const match = disposition.match(/filename="?([^"]+)"?/);
|
||
const filename = match ? match[1] : "importacao_plano_saude.csv";
|
||
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", async () => {
|
||
const listView = document.getElementById("ips-list-view");
|
||
if (!listView) return;
|
||
|
||
const me = await pidGetMe();
|
||
if (!me) return;
|
||
|
||
const utilPerms = (me.permissoes_efetivas && me.permissoes_efetivas["utilitarios"]) || {};
|
||
const canUse = !!(utilPerms.apps && utilPerms.apps["importacao-plano-saude"]);
|
||
|
||
const noAccessEl = document.getElementById("ips-no-access");
|
||
const contentEl = document.getElementById("ips-content");
|
||
if (noAccessEl) noAccessEl.hidden = canUse;
|
||
if (contentEl) contentEl.hidden = !canUse;
|
||
if (!canUse) return;
|
||
|
||
const formView = document.getElementById("ips-form-view");
|
||
const reviewView = document.getElementById("ips-review-view");
|
||
|
||
const listBody = document.getElementById("ips-list-body");
|
||
const listEmpty = document.getElementById("ips-list-empty");
|
||
const listPagination = document.getElementById("ips-list-pagination");
|
||
const listCount = document.getElementById("ips-list-count");
|
||
const listSelectAll = document.getElementById("ips-list-select-all");
|
||
const listBulkActions = document.getElementById("ips-list-bulk-actions");
|
||
const listBulkCount = document.getElementById("ips-list-bulk-count");
|
||
const listBulkDeleteBtn = document.getElementById("ips-list-bulk-delete-btn");
|
||
const listSortHeaders = Array.from(document.querySelectorAll("#ips-list-table th[data-sort]"));
|
||
const newBtn = document.getElementById("ips-new-btn");
|
||
|
||
const formOperadora = document.getElementById("ips-form-operadora");
|
||
const formPlanilha = document.getElementById("ips-form-planilha");
|
||
const formPlanilhaName = document.getElementById("ips-form-planilha-name");
|
||
const formPlanilhaRemoveBtn = document.getElementById("ips-form-planilha-remove");
|
||
const formPlanilhaStatus = document.getElementById("ips-form-planilha-status");
|
||
const formArquivo = document.getElementById("ips-form-arquivo");
|
||
const formArquivoName = document.getElementById("ips-form-arquivo-name");
|
||
const formArquivoRemoveBtn = document.getElementById("ips-form-arquivo-remove");
|
||
const formArquivoStatus = document.getElementById("ips-form-arquivo-status");
|
||
const formTipoMensalidade = document.getElementById("ips-form-tipo-mensalidade");
|
||
const formTipoCoparticipacao = document.getElementById("ips-form-tipo-coparticipacao");
|
||
const formTipoRegraEmpresa = document.getElementById("ips-form-tipo-regra-empresa");
|
||
const custeioMensalidade = document.getElementById("ips-custeio-mensalidade");
|
||
const custeioCoparticipacao = document.getElementById("ips-custeio-coparticipacao");
|
||
const regraEmpresaBox = document.getElementById("ips-regra-empresa-box");
|
||
const regraEmpresaAtual = document.getElementById("ips-regra-empresa-atual");
|
||
const regraEmpresaSelecionarBtn = document.getElementById("ips-regra-empresa-selecionar-btn");
|
||
const regraEmpresaModal = document.getElementById("ips-regra-empresa-modal");
|
||
const regraEmpresaLista = document.getElementById("ips-regra-empresa-lista");
|
||
const regraEmpresaError = document.getElementById("ips-regra-empresa-error");
|
||
const regraEmpresaCancelBtn = document.getElementById("ips-regra-empresa-cancel-btn");
|
||
|
||
// Tela "Nova Importação" — Empresa/Operadora só resolvem uma regra já
|
||
// cadastrada (ver "Cadastro de Regras" abaixo), nunca a editam.
|
||
const impEmpresaSearch = document.getElementById("ips-imp-empresa-search");
|
||
const impEmpresaList = document.getElementById("ips-imp-empresa-combo-list");
|
||
const impEmpresaValor = document.getElementById("ips-imp-empresa-valor");
|
||
const impEmpresaLimparBtn = document.getElementById("ips-imp-empresa-limpar-btn");
|
||
const impEmpresaToggleBtn = document.getElementById("ips-imp-empresa-toggle-btn");
|
||
const impOperadoraSearch = document.getElementById("ips-imp-operadora-search");
|
||
const impOperadoraList = document.getElementById("ips-imp-operadora-combo-list");
|
||
const impOperadoraLimparBtn = document.getElementById("ips-imp-operadora-limpar-btn");
|
||
const impOperadoraToggleBtn = document.getElementById("ips-imp-operadora-toggle-btn");
|
||
const impResumo = document.getElementById("ips-imp-resumo");
|
||
const impResumoTipos = document.getElementById("ips-imp-resumo-tipos");
|
||
const impResumoMensalidade = document.getElementById("ips-imp-resumo-mensalidade");
|
||
const impResumoCoparticipacao = document.getElementById("ips-imp-resumo-coparticipacao");
|
||
const impResumoObservacoes = document.getElementById("ips-imp-resumo-observacoes");
|
||
|
||
// Modal "Cadastro de Regras" — cadastro/edição de RegraCusteioPlanoSaude,
|
||
// separado da execução da importação (ver acima).
|
||
const regracadBtn = document.getElementById("ips-regracad-btn");
|
||
const regracadModal = document.getElementById("ips-regracad-modal");
|
||
const regracadToolbar = document.getElementById("ips-regracad-toolbar");
|
||
const regracadEmpresaSearch = document.getElementById("ips-regracad-empresa-search");
|
||
const regracadEmpresaList = document.getElementById("ips-regracad-empresa-combo-list");
|
||
const regracadEmpresaLimparBtn = document.getElementById("ips-regracad-empresa-limpar-btn");
|
||
const regracadEmpresaToggleBtn = document.getElementById("ips-regracad-empresa-toggle-btn");
|
||
const regracadOperadoraSearch = document.getElementById("ips-regracad-operadora-search");
|
||
const regracadOperadoraList = document.getElementById("ips-regracad-operadora-combo-list");
|
||
const regracadOperadoraLimparBtn = document.getElementById("ips-regracad-operadora-limpar-btn");
|
||
const regracadOperadoraToggleBtn = document.getElementById("ips-regracad-operadora-toggle-btn");
|
||
const regracadNovaBtn = document.getElementById("ips-regracad-nova-btn");
|
||
const regracadNovaOperadoraBtn = document.getElementById("ips-regracad-nova-operadora-btn");
|
||
const regracadVazio = document.getElementById("ips-regracad-vazio");
|
||
const regracadModo = document.getElementById("ips-regracad-modo");
|
||
const regracadNovoView = document.getElementById("ips-regracad-novo-view");
|
||
const regracadNovoCodigoEmpresa = document.getElementById("ips-regracad-novo-codigo-empresa");
|
||
const regracadNovoEmpresaNome = document.getElementById("ips-regracad-novo-empresa-nome");
|
||
const regracadNovoOperadoraSearch = document.getElementById("ips-regracad-novo-operadora-search");
|
||
const regracadNovoOperadoraList = document.getElementById("ips-regracad-novo-operadora-combo-list");
|
||
const regracadNovoOperadoraToggleBtn = document.getElementById("ips-regracad-novo-operadora-toggle-btn");
|
||
const regracadNovoOperadoraDuplicada = document.getElementById("ips-regracad-novo-operadora-duplicada");
|
||
const regracadFormView = document.getElementById("ips-regracad-form-view");
|
||
const regracadObservacoes = document.getElementById("ips-regracad-observacoes");
|
||
const regracadError = document.getElementById("ips-regracad-error");
|
||
const regracadFecharBtn = document.getElementById("ips-regracad-fechar-btn");
|
||
const regracadNovoCancelarBtn = document.getElementById("ips-regracad-novo-cancelar-btn");
|
||
const regracadExcluirBtn = document.getElementById("ips-regracad-excluir-btn");
|
||
const regracadSalvarBtn = document.getElementById("ips-regracad-salvar-btn");
|
||
const regracadCriarBtn = document.getElementById("ips-regracad-criar-btn");
|
||
const regracadConfirmFecharModal = document.getElementById("ips-regracad-confirm-fechar-modal");
|
||
const regracadConfirmFecharCancelarBtn = document.getElementById("ips-regracad-confirm-fechar-cancelar-btn");
|
||
const regracadConfirmFecharOkBtn = document.getElementById("ips-regracad-confirm-fechar-ok-btn");
|
||
|
||
const formError = document.getElementById("ips-form-error");
|
||
const formSubmitBtn = document.getElementById("ips-form-submit-btn");
|
||
const formCancelBtn = document.getElementById("ips-form-cancel-btn");
|
||
const formBackBtn = document.getElementById("ips-form-back-btn");
|
||
|
||
const reviewEmpresa = document.getElementById("ips-review-empresa");
|
||
const reviewSubtitle = document.getElementById("ips-review-subtitle");
|
||
const reviewRegraEmpresaObs = document.getElementById("ips-review-regra-empresa-obs");
|
||
const reviewRegraEmpresaObsLabel = document.getElementById("ips-review-regra-empresa-obs-label");
|
||
const reviewRegraEmpresaObsTexto = document.getElementById("ips-review-regra-empresa-obs-texto");
|
||
const reviewError = document.getElementById("ips-review-error");
|
||
const reviewBackBtn = document.getElementById("ips-review-back-btn");
|
||
const 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 = [];
|
||
// Regras empresa (portal_api.planos_saude.regras_empresa) — registro fixo
|
||
// no código; regraEmpresaSelecionada guarda {key, label} da regra
|
||
// atualmente escolhida no checkbox "Mensalidade usa regra especial" (ver
|
||
// ips-regra-empresa-modal), dentro do formulário de Cadastro de Regras.
|
||
let regrasEmpresaCache = [];
|
||
let regraEmpresaSelecionada = null;
|
||
// Banco de regras de custeio cadastradas por empresa+operadora — fonte
|
||
// única tanto pra tela "Nova Importação" (só resolve/aplica) quanto pra
|
||
// "Cadastro de Regras" (edita).
|
||
let regras = [];
|
||
// Tela "Nova Importação": a regra resolvida a partir da Empresa+Operadora
|
||
// escolhidas (no máximo uma por combinação, ver unique_together no
|
||
// backend) — é o que o submit envia, sem nenhum campo editável na tela.
|
||
let regraResolvidaAtual = null;
|
||
// Modal "Cadastro de Regras": empresa/operadora escolhidas no modo
|
||
// "navegar" (edição de uma regra existente) e a regra resolvida por elas;
|
||
// regracadModoNovo alterna pro formulário de criação (empresa livre +
|
||
// operadora sem restrição).
|
||
let regracadEmpresaAtual = null;
|
||
let regracadOperadoraAtual = null;
|
||
let regracadRegraEmEdicao = null;
|
||
let regracadModoNovo = false;
|
||
let regracadNovoOperadoraKey = null;
|
||
|
||
// Compara valores de uma célula da tabela de revisão pra ordenação por
|
||
// coluna — números no formato BR (milhar com ponto, decimal com vírgula,
|
||
// ex.: "1.234,56") são comparados numericamente; qualquer outra coisa
|
||
// (nome, CPF com hífen, data dd/mm/aaaa) cai pra comparação de texto.
|
||
function comparaValorRevisao(a, b) {
|
||
const paraNumero = (valor) => {
|
||
if (valor === null || valor === undefined || valor === "") return NaN;
|
||
const texto = String(valor).trim();
|
||
if (!/^-?[\d.,]+$/.test(texto)) return NaN;
|
||
return Number(texto.replace(/\./g, "").replace(",", "."));
|
||
};
|
||
const an = paraNumero(a);
|
||
const bn = paraNumero(b);
|
||
if (!Number.isNaN(an) && !Number.isNaN(bn)) return an - bn;
|
||
return String(a || "").localeCompare(String(b || ""), "pt-BR", { sensitivity: "base" });
|
||
}
|
||
|
||
/**
|
||
* Valida um único arquivo (planilha padrão OU arquivo da operadora) assim
|
||
* que o colaborador anexa, chamando /validar-arquivo/ — mesmo parser que
|
||
* seria usado no "Processar", só que sobre um upload descartável, pra
|
||
* apontar exatamente qual dos dois documentos está fora do padrão antes de
|
||
* precisar dos dois juntos. `ehInvalido()` (usado na validação do submit)
|
||
* só conta como inválido depois que a resposta chega — enquanto pendente
|
||
* ou sem arquivo, não bloqueia o envio (o create() no servidor valida de
|
||
* novo, é só essa checagem antecipada que não é garantida).
|
||
*/
|
||
function criarValidadorArquivo({ input, nameEl, removeBtn, statusEl, tipo, getOperadoraKey }) {
|
||
let estadoAtual = null;
|
||
|
||
function limpar() {
|
||
input.value = "";
|
||
nameEl.textContent = "Nenhum arquivo selecionado";
|
||
removeBtn.hidden = true;
|
||
statusEl.hidden = true;
|
||
statusEl.className = "ips-file-field__status";
|
||
statusEl.textContent = "";
|
||
estadoAtual = null;
|
||
}
|
||
|
||
async function validar() {
|
||
const arquivo = input.files[0];
|
||
if (!arquivo) {
|
||
limpar();
|
||
return;
|
||
}
|
||
nameEl.textContent = arquivo.name;
|
||
removeBtn.hidden = false;
|
||
statusEl.hidden = false;
|
||
statusEl.className = "ips-file-field__status ips-file-field__status--pendente";
|
||
statusEl.textContent = "Validando arquivo...";
|
||
estadoAtual = null;
|
||
|
||
try {
|
||
const operadoraKey = getOperadoraKey ? getOperadoraKey() : null;
|
||
const resultado = await pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey);
|
||
estadoAtual = resultado;
|
||
statusEl.className = `ips-file-field__status ${resultado.valido ? "ips-file-field__status--ok" : "ips-file-field__status--erro"}`;
|
||
statusEl.textContent = `${resultado.valido ? "✓" : "✗"} ${resultado.mensagem}`;
|
||
} catch (e) {
|
||
estadoAtual = { valido: false, mensagem: e.message };
|
||
statusEl.className = "ips-file-field__status ips-file-field__status--erro";
|
||
statusEl.textContent = `✗ ${e.message}`;
|
||
}
|
||
}
|
||
|
||
input.addEventListener("change", validar);
|
||
removeBtn.addEventListener("click", limpar);
|
||
|
||
return {
|
||
limpar,
|
||
revalidarSeAnexado: () => {
|
||
if (input.files[0]) validar();
|
||
},
|
||
ehInvalido: () => estadoAtual !== null && estadoAtual.valido === false,
|
||
};
|
||
}
|
||
|
||
const validadorPlanilha = criarValidadorArquivo({
|
||
input: formPlanilha,
|
||
nameEl: formPlanilhaName,
|
||
removeBtn: formPlanilhaRemoveBtn,
|
||
statusEl: formPlanilhaStatus,
|
||
tipo: "planilha",
|
||
});
|
||
|
||
const validadorArquivo = criarValidadorArquivo({
|
||
input: formArquivo,
|
||
nameEl: formArquivoName,
|
||
removeBtn: formArquivoRemoveBtn,
|
||
statusEl: formArquivoStatus,
|
||
tipo: "operadora",
|
||
getOperadoraKey: () => formOperadora.value,
|
||
});
|
||
|
||
function 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();
|
||
}
|
||
|
||
// "Regra empresa" (terceiro tipo de importação, mutuamente exclusivo com
|
||
// "Mensalidade" — as duas são formas alternativas de configurar o mesmo
|
||
// tipo de lançamento "mensalidade", ver regras_empresa.py) — zera o
|
||
// checkbox, esconde a caixa e limpa a seleção em memória.
|
||
function renderRegraEmpresaAtual() {
|
||
if (!regraEmpresaAtual) return;
|
||
regraEmpresaAtual.textContent = regraEmpresaSelecionada
|
||
? `Regra selecionada: ${regraEmpresaSelecionada.label}`
|
||
: "Nenhuma regra selecionada.";
|
||
}
|
||
|
||
function limparRegraEmpresa() {
|
||
if (formTipoRegraEmpresa) formTipoRegraEmpresa.checked = false;
|
||
if (regraEmpresaBox) regraEmpresaBox.hidden = true;
|
||
regraEmpresaSelecionada = null;
|
||
renderRegraEmpresaAtual();
|
||
}
|
||
|
||
// Zera a parte de tipo/custeio do formulário de Cadastro de Regras
|
||
// (tipos_lancamento + modo de custeio/limite/percentual de cada
|
||
// combinação tipo×pessoa) — usado ao entrar no modo "+ Nova regra".
|
||
function limparCusteioForm() {
|
||
formTipoMensalidade.checked = false;
|
||
formTipoCoparticipacao.checked = false;
|
||
limparRegraEmpresa();
|
||
custeioMensalidade.hidden = true;
|
||
custeioCoparticipacao.hidden = true;
|
||
custeioMensalidade.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false));
|
||
custeioCoparticipacao.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false));
|
||
PID_IPS_TIPOS.forEach((tipo) => {
|
||
PID_IPS_PESSOAS.forEach((pessoa) => {
|
||
const chave = `${tipo}-${pessoa}`;
|
||
const regraBox = document.getElementById(`ips-regra-especifica-${chave}`);
|
||
const limiteInput = document.getElementById(`ips-limite-${chave}`);
|
||
const percentualInput = document.getElementById(`ips-percentual-${chave}`);
|
||
if (regraBox) regraBox.hidden = true;
|
||
if (limiteInput) limiteInput.value = "";
|
||
if (percentualInput) percentualInput.value = "";
|
||
});
|
||
});
|
||
}
|
||
|
||
// "150,00" / 150 / 150.5 -> "150,00"/"150,50" — usado pra repopular os
|
||
// inputs de limite/percentual (texto BR) a partir de uma regra salva, cujo
|
||
// custeio_por_tipo já vem validado/parseado como float pelo backend.
|
||
function formatarNumeroBr(valor) {
|
||
if (valor === null || valor === undefined || valor === "") return "";
|
||
const numero = Number(valor);
|
||
return Number.isNaN(numero) ? String(valor) : numero.toFixed(2).replace(".", ",");
|
||
}
|
||
|
||
/**
|
||
* Lê o custeio atualmente preenchido no formulário (quais tipos de
|
||
* lançamento estão marcados + o modo de custeio/limite/percentual de cada
|
||
* combinação tipo×pessoa) — mesmo formato de tipos_lancamento/custeio_por_tipo
|
||
* de RegraCusteioPlanoSaude, usado tanto pra montar o payload de "Salvar
|
||
* regra atual..." quanto (via os mesmos campos) pro submit da importação.
|
||
*/
|
||
function coletarCusteioAtual() {
|
||
const tipos_lancamento = [];
|
||
if (formTipoMensalidade.checked || formTipoRegraEmpresa.checked) tipos_lancamento.push("mensalidade");
|
||
if (formTipoCoparticipacao.checked) tipos_lancamento.push("coparticipacao");
|
||
|
||
const custeio_por_tipo = {};
|
||
PID_IPS_TIPOS.forEach((tipo) => {
|
||
custeio_por_tipo[tipo] = {};
|
||
// "Regra empresa" substitui o custeio manual de mensalidade — não há
|
||
// titular/dependente pra ler aqui (ver regraEmpresaSelecionada).
|
||
if (tipo === "mensalidade" && formTipoRegraEmpresa.checked) return;
|
||
PID_IPS_PESSOAS.forEach((pessoa) => {
|
||
const chave = `${tipo}-${pessoa}`;
|
||
const escolhido = document.querySelector(`input[name="ips-custeio-${chave}"]:checked`);
|
||
if (!escolhido) return;
|
||
const entrada = { modo: escolhido.value };
|
||
if (escolhido.value === "especifica") {
|
||
entrada.limite_valor = document.getElementById(`ips-limite-${chave}`).value.trim();
|
||
entrada.percentual = document.getElementById(`ips-percentual-${chave}`).value.trim();
|
||
}
|
||
custeio_por_tipo[tipo][pessoa] = entrada;
|
||
});
|
||
});
|
||
|
||
return { tipos_lancamento, custeio_por_tipo };
|
||
}
|
||
|
||
// Valida se o custeio dos tipos marcados está completo (titular e
|
||
// dependente de cada um) — reaproveitado pelo submit da importação e por
|
||
// "Salvar regra atual...", já que as duas ações exigem a mesma coisa:
|
||
// um custeio_por_tipo completo pros tipos selecionados.
|
||
function mensagemErroCusteio(tipos) {
|
||
if (!tipos.length) return "Selecione ao menos um tipo de importação.";
|
||
if (formTipoRegraEmpresa.checked && !regraEmpresaSelecionada) {
|
||
return "Selecione a regra empresa a aplicar.";
|
||
}
|
||
for (const tipo of tipos) {
|
||
if (tipo === "mensalidade" && formTipoRegraEmpresa.checked) continue;
|
||
for (const pessoa of PID_IPS_PESSOAS) {
|
||
const chave = `${tipo}-${pessoa}`;
|
||
const rotulo = `${PID_IPS_TIPO_LABELS[tipo]} (${PID_IPS_PESSOA_LABELS[pessoa]})`;
|
||
const escolhido = document.querySelector(`input[name="ips-custeio-${chave}"]:checked`);
|
||
if (!escolhido) return `Informe o custeio para ${rotulo}.`;
|
||
if (escolhido.value === "especifica") {
|
||
const limite = document.getElementById(`ips-limite-${chave}`).value.trim();
|
||
const percentual = document.getElementById(`ips-percentual-${chave}`).value.trim();
|
||
if (!limite && !percentual) return `Informe o limite de valor e/ou o percentual de custeio para ${rotulo}.`;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Repõe no formulário uma regra salva (RegraCusteioPlanoSaude) — os campos
|
||
// continuam 100% editáveis depois, é só um preenchimento em massa, não um
|
||
// modo "somente leitura". `regra.custeio_por_tipo` já vem validado do
|
||
// backend (limite_valor/percentual como float, não texto BR).
|
||
function aplicarCusteio(regra) {
|
||
const tipos = Array.isArray(regra.tipos_lancamento) ? regra.tipos_lancamento : [];
|
||
// Repõe o custeio de uma regra dentro do formulário de Cadastro de
|
||
// Regras — mutuamente exclusivo entre "mensalidade manual" e "mensalidade
|
||
// usa regra especial" (ver limparRegraEmpresa()).
|
||
limparRegraEmpresa();
|
||
if (regra.regra_empresa_chave) {
|
||
const re = regrasEmpresaCache.find((r) => r.key === regra.regra_empresa_chave);
|
||
regraEmpresaSelecionada = re || { key: regra.regra_empresa_chave, label: regra.regra_empresa_chave };
|
||
if (formTipoRegraEmpresa) formTipoRegraEmpresa.checked = true;
|
||
if (regraEmpresaBox) regraEmpresaBox.hidden = false;
|
||
renderRegraEmpresaAtual();
|
||
}
|
||
formTipoMensalidade.checked = tipos.includes("mensalidade") && !regra.regra_empresa_chave;
|
||
custeioMensalidade.hidden = !formTipoMensalidade.checked;
|
||
formTipoCoparticipacao.checked = tipos.includes("coparticipacao");
|
||
custeioCoparticipacao.hidden = !formTipoCoparticipacao.checked;
|
||
|
||
const custeioPorTipo = regra.custeio_por_tipo || {};
|
||
PID_IPS_TIPOS.forEach((tipo) => {
|
||
PID_IPS_PESSOAS.forEach((pessoa) => {
|
||
const chave = `${tipo}-${pessoa}`;
|
||
const regraBox = document.getElementById(`ips-regra-especifica-${chave}`);
|
||
const limiteInput = document.getElementById(`ips-limite-${chave}`);
|
||
const percentualInput = document.getElementById(`ips-percentual-${chave}`);
|
||
|
||
document.querySelectorAll(`input[name="ips-custeio-${chave}"]`).forEach((r) => (r.checked = false));
|
||
if (limiteInput) limiteInput.value = "";
|
||
if (percentualInput) percentualInput.value = "";
|
||
if (regraBox) regraBox.hidden = true;
|
||
|
||
const entrada = custeioPorTipo[tipo] && custeioPorTipo[tipo][pessoa];
|
||
if (!entrada || !entrada.modo) return;
|
||
const radio = document.querySelector(`input[name="ips-custeio-${chave}"][value="${entrada.modo}"]`);
|
||
if (radio) radio.checked = true;
|
||
if (entrada.modo === "especifica") {
|
||
if (limiteInput) limiteInput.value = formatarNumeroBr(entrada.limite_valor);
|
||
if (percentualInput) percentualInput.value = formatarNumeroBr(entrada.percentual);
|
||
if (regraBox) regraBox.hidden = false;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function operadoraLabel(key) {
|
||
const operadora = operadorasCache.find((op) => op.key === key);
|
||
return operadora ? operadora.label : key;
|
||
}
|
||
|
||
// Compara códigos de empresa (string) numericamente quando os dois são
|
||
// numéricos — evita "1006" aparecer antes de "129" por comparação de
|
||
// string pura (mesmo espírito de comparaValoresLista, dedicado aos
|
||
// comboboxes de "Empresa" abaixo).
|
||
function compararCodigoEmpresa(a, b) {
|
||
const an = Number(a);
|
||
const bn = Number(b);
|
||
if (!Number.isNaN(an) && !Number.isNaN(bn)) return an - bn;
|
||
return String(a).localeCompare(String(b), "pt-BR");
|
||
}
|
||
|
||
// "<código> - <nome>" quando o nome já foi resolvido do Questor (ver
|
||
// RegraCusteioPlanoSaudeSerializer.get_nome_empresa) — mesmo padrão que o
|
||
// combobox de Operadora já usa pro próprio nome. Sem nome resolvido
|
||
// ainda, mostra só o código (nunca bloqueia navegar/criar regra).
|
||
function labelEmpresa(codigo) {
|
||
const comNome = regras.find((r) => r.codigo_empresa === codigo && r.nome_empresa);
|
||
return comNome ? `${codigo} - ${comNome.nome_empresa}` : codigo;
|
||
}
|
||
|
||
function empresasComRegra() {
|
||
const codigos = [...new Set(regras.map((r) => r.codigo_empresa))].sort(compararCodigoEmpresa);
|
||
return codigos.map((codigo) => ({ key: codigo, label: labelEmpresa(codigo) }));
|
||
}
|
||
|
||
function operadorasComRegraParaEmpresa(codigoEmpresa) {
|
||
return regras
|
||
.filter((r) => r.codigo_empresa === codigoEmpresa)
|
||
.map((r) => ({ key: r.operadora, label: operadoraLabel(r.operadora) }));
|
||
}
|
||
|
||
/**
|
||
* Fábrica genérica de combobox pesquisável (campo de texto dobrando como
|
||
* "display" do valor selecionado + lista flutuante filtrada por texto,
|
||
* mesmas classes .ips-combo*) — reaproveitada pelos comboboxes de
|
||
* Empresa/Operadora de "Nova Importação" (só resolve uma regra já
|
||
* cadastrada) e de "Cadastro de Regras" (navegar entre regras existentes
|
||
* ou escolher a operadora de uma regra nova), no lugar do antigo par de
|
||
* comboboxes hardcoded (Operadora/Regra de custeio salva) que duplicava a
|
||
* mesma lógica de busca+seleção duas vezes.
|
||
*/
|
||
function criarComboboxTexto({ input, list, clearBtn, toggleBtn, onChange }) {
|
||
let itens = [];
|
||
const comboEl = input ? input.closest(".ips-combo") : null;
|
||
|
||
function fechar() {
|
||
if (list) {
|
||
list.hidden = true;
|
||
list.innerHTML = "";
|
||
}
|
||
}
|
||
|
||
function renderLista(lista) {
|
||
if (!list) return;
|
||
list.innerHTML = lista.length
|
||
? lista
|
||
.map((it) => `<div class="ips-combo__item" data-combo-key="${escapeAttr(String(it.key))}">${escapeHtml(it.label)}</div>`)
|
||
.join("")
|
||
: `<p class="ips-combo__empty">Nenhum resultado encontrado.</p>`;
|
||
list.hidden = false;
|
||
}
|
||
|
||
function render(filtro) {
|
||
const termo = (filtro || "").trim().toLowerCase();
|
||
renderLista(termo ? itens.filter((it) => it.label.toLowerCase().includes(termo)) : itens);
|
||
}
|
||
|
||
// Botão "x" embutido na própria barra do combobox (dentro de
|
||
// .ips-combo) — só aparece quando há algum texto (digitado ou
|
||
// selecionado) pra limpar.
|
||
function atualizarClearBtn() {
|
||
if (clearBtn) clearBtn.hidden = !(input && input.value.trim());
|
||
}
|
||
|
||
function selecionar(item, { silencioso } = {}) {
|
||
if (input) input.value = item ? item.label : "";
|
||
atualizarClearBtn();
|
||
fechar();
|
||
if (!silencioso && onChange) onChange(item || null);
|
||
}
|
||
|
||
if (input) {
|
||
input.addEventListener("focus", () => render(input.value));
|
||
input.addEventListener("input", () => {
|
||
render(input.value);
|
||
atualizarClearBtn();
|
||
if (onChange) onChange(null);
|
||
});
|
||
}
|
||
if (list) {
|
||
list.addEventListener("click", (event) => {
|
||
const el = event.target.closest("[data-combo-key]");
|
||
if (!el) return;
|
||
const item = itens.find((it) => String(it.key) === el.getAttribute("data-combo-key"));
|
||
if (item) selecionar(item);
|
||
});
|
||
}
|
||
if (clearBtn) {
|
||
// Diferente de limpar() (silencioso, usado só por quem já vai tratar
|
||
// a cascata na mão) — clicar no "x" precisa disparar onChange(null)
|
||
// normalmente, pra "limpar Empresa também limpa Operadora" continuar
|
||
// funcionando (ver regracadEmpresaOps.onChange).
|
||
clearBtn.addEventListener("click", () => {
|
||
selecionar(null);
|
||
if (input) input.focus();
|
||
});
|
||
atualizarClearBtn();
|
||
}
|
||
if (toggleBtn) {
|
||
// Botão embutido na barra, ao lado do "x" — pedido explícito do
|
||
// usuário: mostra a lista completa igual a quando o campo está vazio;
|
||
// se já houver algo selecionado, mostra as OUTRAS opções cadastradas
|
||
// (sem repetir a já escolhida), já que só focar o campo com um valor
|
||
// preenchido filtraria pra mostrar de novo só o item já selecionado.
|
||
toggleBtn.addEventListener("click", () => {
|
||
if (input) input.focus();
|
||
const atual = input ? input.value.trim() : "";
|
||
renderLista(atual ? itens.filter((it) => it.label !== atual) : itens);
|
||
});
|
||
}
|
||
|
||
return {
|
||
setItens(novos) {
|
||
itens = novos || [];
|
||
},
|
||
selecionar,
|
||
limpar() {
|
||
selecionar(null, { silencioso: true });
|
||
},
|
||
fechar,
|
||
contains(el) {
|
||
return !!comboEl && comboEl.contains(el);
|
||
},
|
||
};
|
||
}
|
||
|
||
const impOperadoraOps = criarComboboxTexto({
|
||
input: impOperadoraSearch,
|
||
list: impOperadoraList,
|
||
clearBtn: impOperadoraLimparBtn,
|
||
toggleBtn: impOperadoraToggleBtn,
|
||
onChange: (item) => {
|
||
formOperadora.value = item ? item.key : "";
|
||
const empresaAtual = impEmpresaValor.value;
|
||
regraResolvidaAtual = item
|
||
? regras.find((r) => r.codigo_empresa === empresaAtual && r.operadora === item.key) || null
|
||
: null;
|
||
renderResumoRegra(regraResolvidaAtual);
|
||
if (item) validadorArquivo.revalidarSeAnexado();
|
||
},
|
||
});
|
||
|
||
const impEmpresaOps = criarComboboxTexto({
|
||
input: impEmpresaSearch,
|
||
list: impEmpresaList,
|
||
clearBtn: impEmpresaLimparBtn,
|
||
toggleBtn: impEmpresaToggleBtn,
|
||
onChange: (item) => {
|
||
impEmpresaValor.value = item ? item.key : "";
|
||
impOperadoraOps.limpar();
|
||
if (item) {
|
||
impOperadoraOps.setItens(operadorasComRegraParaEmpresa(item.key));
|
||
if (impOperadoraSearch) impOperadoraSearch.disabled = false;
|
||
if (impOperadoraToggleBtn) impOperadoraToggleBtn.disabled = false;
|
||
} else {
|
||
impOperadoraOps.setItens([]);
|
||
if (impOperadoraSearch) impOperadoraSearch.disabled = true;
|
||
if (impOperadoraToggleBtn) impOperadoraToggleBtn.disabled = true;
|
||
}
|
||
},
|
||
});
|
||
|
||
// Explica o custeio de uma regra em texto simples pro resumo só-leitura
|
||
// de "Nova Importação" — nunca editável ali, só exibido.
|
||
function resumoModoPessoa(entrada) {
|
||
if (!entrada || !entrada.modo) return "não configurado";
|
||
if (entrada.modo === "empresa") return "custeado pela empresa";
|
||
if (entrada.modo === "empregado") return "descontado do empregado";
|
||
if (entrada.modo === "especifica") {
|
||
const partes = [];
|
||
if (entrada.limite_valor !== null && entrada.limite_valor !== undefined) {
|
||
partes.push(`limite de R$ ${formatarNumeroBr(entrada.limite_valor)}`);
|
||
}
|
||
if (entrada.percentual !== null && entrada.percentual !== undefined) {
|
||
partes.push(`${formatarNumeroBr(entrada.percentual)}% custeado pela empresa`);
|
||
}
|
||
return `regra específica (${partes.join(", ") || "sem detalhe"})`;
|
||
}
|
||
return entrada.modo;
|
||
}
|
||
|
||
function renderResumoRegra(regra) {
|
||
if (!impResumo) return;
|
||
if (!regra) {
|
||
impResumo.hidden = true;
|
||
return;
|
||
}
|
||
const tipos = regra.tipos_lancamento || [];
|
||
const custeioPorTipo = regra.custeio_por_tipo || {};
|
||
impResumo.hidden = false;
|
||
if (impResumoTipos) impResumoTipos.textContent = `Tipos cobertos por esta regra: ${tiposLabel(tipos)}`;
|
||
|
||
if (impResumoMensalidade) {
|
||
if (tipos.includes("mensalidade")) {
|
||
if (regra.regra_empresa_chave) {
|
||
const re = regrasEmpresaCache.find((r) => r.key === regra.regra_empresa_chave);
|
||
impResumoMensalidade.textContent = `Mensalidade: usa regra especial da empresa — ${re ? re.label : regra.regra_empresa_chave}`;
|
||
} else {
|
||
const titular = resumoModoPessoa(custeioPorTipo.mensalidade && custeioPorTipo.mensalidade.titular);
|
||
const dependente = resumoModoPessoa(custeioPorTipo.mensalidade && custeioPorTipo.mensalidade.dependente);
|
||
impResumoMensalidade.textContent = `Mensalidade — Titular: ${titular}. Dependente: ${dependente}.`;
|
||
}
|
||
impResumoMensalidade.hidden = false;
|
||
} else {
|
||
impResumoMensalidade.hidden = true;
|
||
}
|
||
}
|
||
|
||
if (impResumoCoparticipacao) {
|
||
if (tipos.includes("coparticipacao")) {
|
||
const titular = resumoModoPessoa(custeioPorTipo.coparticipacao && custeioPorTipo.coparticipacao.titular);
|
||
const dependente = resumoModoPessoa(custeioPorTipo.coparticipacao && custeioPorTipo.coparticipacao.dependente);
|
||
impResumoCoparticipacao.textContent = `Coparticipação — Titular: ${titular}. Dependente: ${dependente}.`;
|
||
impResumoCoparticipacao.hidden = false;
|
||
} else {
|
||
impResumoCoparticipacao.hidden = true;
|
||
}
|
||
}
|
||
|
||
if (impResumoObservacoes) {
|
||
if (regra.observacoes) {
|
||
impResumoObservacoes.textContent = `Observações: ${regra.observacoes}`;
|
||
impResumoObservacoes.hidden = false;
|
||
} else {
|
||
impResumoObservacoes.hidden = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Monta o FormData de "Nova Importação" a partir da regra resolvida —
|
||
// mesmo formato de campos que ImportacaoPlanoSaudeCreateSerializer já
|
||
// espera (custeio_<tipo>_<pessoa>, limite_valor_.../percentual_...), só
|
||
// que lendo do objeto da regra em memória em vez de inputs editáveis na
|
||
// tela (que não existem mais aqui).
|
||
function montarFormDataDeRegra(formData, regra) {
|
||
formData.append("operadora", regra.operadora);
|
||
formData.append("tipos_lancamento", (regra.tipos_lancamento || []).join(","));
|
||
formData.append("regra_custeio_salva", regra.id);
|
||
if (regra.regra_empresa_chave) {
|
||
formData.append("regra_empresa", regra.regra_empresa_chave);
|
||
}
|
||
(regra.tipos_lancamento || []).forEach((tipo) => {
|
||
if (tipo === "mensalidade" && regra.regra_empresa_chave) return;
|
||
PID_IPS_PESSOAS.forEach((pessoa) => {
|
||
const entrada = (regra.custeio_por_tipo && regra.custeio_por_tipo[tipo] && regra.custeio_por_tipo[tipo][pessoa]) || {};
|
||
formData.append(`custeio_${tipo}_${pessoa}`, entrada.modo || "");
|
||
if (entrada.modo === "especifica") {
|
||
formData.append(`limite_valor_${tipo}_${pessoa}`, formatarNumeroBr(entrada.limite_valor));
|
||
formData.append(`percentual_${tipo}_${pessoa}`, formatarNumeroBr(entrada.percentual));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
async function refreshRegras() {
|
||
try {
|
||
regras = await pidFetchRegrasCusteioPlanoSaude();
|
||
} catch (e) {
|
||
regras = [];
|
||
}
|
||
}
|
||
|
||
// Lista do modal "Selecionar regra empresa" — registro fixo em
|
||
// portal_api.planos_saude.regras_empresa, sem "Excluir"/"Editar" (só o
|
||
// desenvolvedor cadastra uma regra nova, direto no código).
|
||
function renderRegraEmpresaLista() {
|
||
if (!regraEmpresaLista) return;
|
||
if (!regrasEmpresaCache.length) {
|
||
regraEmpresaLista.innerHTML = `<p class="checklist-empty">Nenhuma regra empresa cadastrada.</p>`;
|
||
return;
|
||
}
|
||
regraEmpresaLista.innerHTML = regrasEmpresaCache
|
||
.map(
|
||
(r) => `
|
||
<div class="ips-regra-row">
|
||
<div class="ips-regra-row__info">
|
||
<p class="ips-regra-row__nome">${escapeHtml(r.label)}</p>
|
||
</div>
|
||
<div class="ips-regra-row__actions">
|
||
<button type="button" class="btn-outline" data-regra-empresa-selecionar="${r.key}">Selecionar</button>
|
||
</div>
|
||
</div>`
|
||
)
|
||
.join("");
|
||
}
|
||
|
||
// Modal "Cadastro de Regras" — navegar (empresa+operadora resolvem no
|
||
// máximo uma regra, ver unique_together no backend) ou criar uma nova.
|
||
const regracadOperadoraOps = criarComboboxTexto({
|
||
input: regracadOperadoraSearch,
|
||
list: regracadOperadoraList,
|
||
clearBtn: regracadOperadoraLimparBtn,
|
||
toggleBtn: regracadOperadoraToggleBtn,
|
||
onChange: (item) => {
|
||
regracadOperadoraAtual = item ? item.key : null;
|
||
if (item && regracadEmpresaAtual) {
|
||
const regra = regras.find((r) => r.codigo_empresa === regracadEmpresaAtual && r.operadora === item.key);
|
||
if (regra) {
|
||
regracadCarregarParaEdicao(regra);
|
||
return;
|
||
}
|
||
}
|
||
regracadRegraEmEdicao = null;
|
||
regracadMostrarVazio();
|
||
},
|
||
});
|
||
|
||
const regracadEmpresaOps = criarComboboxTexto({
|
||
input: regracadEmpresaSearch,
|
||
list: regracadEmpresaList,
|
||
clearBtn: regracadEmpresaLimparBtn,
|
||
toggleBtn: regracadEmpresaToggleBtn,
|
||
onChange: (item) => {
|
||
regracadEmpresaAtual = item ? item.key : null;
|
||
regracadOperadoraOps.limpar();
|
||
if (item) {
|
||
regracadOperadoraOps.setItens(operadorasComRegraParaEmpresa(item.key));
|
||
if (regracadOperadoraSearch) regracadOperadoraSearch.disabled = false;
|
||
if (regracadOperadoraToggleBtn) regracadOperadoraToggleBtn.disabled = false;
|
||
if (regracadNovaOperadoraBtn) regracadNovaOperadoraBtn.hidden = false;
|
||
} else {
|
||
regracadOperadoraOps.setItens([]);
|
||
if (regracadOperadoraSearch) regracadOperadoraSearch.disabled = true;
|
||
if (regracadOperadoraToggleBtn) regracadOperadoraToggleBtn.disabled = true;
|
||
if (regracadNovaOperadoraBtn) regracadNovaOperadoraBtn.hidden = true;
|
||
}
|
||
regracadRegraEmEdicao = null;
|
||
regracadMostrarVazio();
|
||
},
|
||
});
|
||
|
||
const regracadNovoOperadoraOps = criarComboboxTexto({
|
||
input: regracadNovoOperadoraSearch,
|
||
list: regracadNovoOperadoraList,
|
||
toggleBtn: regracadNovoOperadoraToggleBtn,
|
||
onChange: (item) => {
|
||
regracadNovoOperadoraKey = item ? item.key : null;
|
||
regracadAtualizarNovoOperadoraDuplicada();
|
||
},
|
||
});
|
||
|
||
// Mensagem "Editando regra existente"/"Cadastrando regra nova" acima do
|
||
// formulário de custeio — pedido explícito do usuário, pra não confundir
|
||
// os dois modos. `regracadVazio` (nenhum dos dois) não mostra nada.
|
||
function regracadSetModo(texto, classeExtra) {
|
||
if (!regracadModo) return;
|
||
if (!texto) {
|
||
regracadModo.hidden = true;
|
||
regracadModo.textContent = "";
|
||
regracadModo.className = "ips-regracad-modo";
|
||
return;
|
||
}
|
||
regracadModo.hidden = false;
|
||
regracadModo.textContent = texto;
|
||
regracadModo.className = `ips-regracad-modo ${classeExtra}`;
|
||
}
|
||
|
||
function regracadMostrarVazio() {
|
||
if (regracadToolbar) regracadToolbar.hidden = false;
|
||
if (regracadVazio) regracadVazio.hidden = false;
|
||
if (regracadNovoView) regracadNovoView.hidden = true;
|
||
if (regracadFormView) regracadFormView.hidden = true;
|
||
if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.hidden = true;
|
||
if (regracadSalvarBtn) regracadSalvarBtn.hidden = true;
|
||
if (regracadExcluirBtn) regracadExcluirBtn.hidden = true;
|
||
if (regracadCriarBtn) regracadCriarBtn.hidden = true;
|
||
if (regracadError) regracadError.textContent = "";
|
||
regracadSetModo(null);
|
||
}
|
||
|
||
function regracadCarregarParaEdicao(regra) {
|
||
regracadModoNovo = false;
|
||
regracadRegraEmEdicao = regra;
|
||
if (regracadToolbar) regracadToolbar.hidden = false;
|
||
if (regracadNovoView) regracadNovoView.hidden = true;
|
||
if (regracadVazio) regracadVazio.hidden = true;
|
||
if (regracadFormView) regracadFormView.hidden = false;
|
||
aplicarCusteio(regra);
|
||
if (regracadObservacoes) regracadObservacoes.value = regra.observacoes || "";
|
||
if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.hidden = true;
|
||
if (regracadSalvarBtn) regracadSalvarBtn.hidden = false;
|
||
if (regracadExcluirBtn) regracadExcluirBtn.hidden = false;
|
||
if (regracadCriarBtn) regracadCriarBtn.hidden = true;
|
||
if (regracadError) regracadError.textContent = "";
|
||
const empresaLabel = regra.nome_empresa ? `${regra.codigo_empresa} - ${regra.nome_empresa}` : regra.codigo_empresa;
|
||
regracadSetModo(`Editando regra existente: ${empresaLabel} · ${operadoraLabel(regra.operadora)}`, "ips-regracad-modo--editando");
|
||
}
|
||
|
||
// Volta pro estado de navegação (empresa/operadora do topo) a partir do
|
||
// que já estava selecionado ali — usado por regracadMostrarVazio() (via
|
||
// onChange dos combos) e por "Cancelar" em "+ Nova regra".
|
||
function regracadMostrarConformeSelecaoAtual() {
|
||
if (regracadEmpresaAtual && regracadOperadoraAtual) {
|
||
const regra = regras.find(
|
||
(r) => r.codigo_empresa === regracadEmpresaAtual && r.operadora === regracadOperadoraAtual
|
||
);
|
||
if (regra) {
|
||
regracadCarregarParaEdicao(regra);
|
||
return;
|
||
}
|
||
}
|
||
regracadMostrarVazio();
|
||
}
|
||
|
||
// Nome da empresa (Questor), ao lado do campo "Código da empresa" em "+
|
||
// Nova regra" — resolvido/cacheado no backend (ver GET /nome-empresa/),
|
||
// não bloqueia criar a regra se não encontrar nada.
|
||
function limparNomeEmpresaNovo() {
|
||
if (!regracadNovoEmpresaNome) return;
|
||
regracadNovoEmpresaNome.hidden = true;
|
||
regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome";
|
||
regracadNovoEmpresaNome.textContent = "";
|
||
}
|
||
|
||
async function atualizarNomeEmpresaNovo() {
|
||
if (regracadNomeEmpresaDebounce) {
|
||
clearTimeout(regracadNomeEmpresaDebounce);
|
||
regracadNomeEmpresaDebounce = null;
|
||
}
|
||
if (!regracadNovoCodigoEmpresa || !regracadNovoEmpresaNome) return;
|
||
const codigo = regracadNovoCodigoEmpresa.value.trim();
|
||
if (!codigo) {
|
||
limparNomeEmpresaNovo();
|
||
return;
|
||
}
|
||
regracadNovoEmpresaNome.hidden = false;
|
||
regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome ips-regracad-novo__empresa-nome--buscando";
|
||
regracadNovoEmpresaNome.textContent = "Buscando nome da empresa...";
|
||
let resultado = null;
|
||
try {
|
||
resultado = await pidBuscarNomeEmpresaPlanoSaude(codigo);
|
||
} catch (e) {
|
||
resultado = null;
|
||
}
|
||
// O código pode ter mudado enquanto a busca estava em andamento —
|
||
// nesse caso, quem disparou de novo já está atualizando o texto.
|
||
if (regracadNovoCodigoEmpresa.value.trim() !== codigo) return;
|
||
if (resultado && resultado.nome_empresa) {
|
||
regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome";
|
||
regracadNovoEmpresaNome.textContent = resultado.nome_empresa;
|
||
} else {
|
||
regracadNovoEmpresaNome.className = "ips-regracad-novo__empresa-nome ips-regracad-novo__empresa-nome--nao-encontrada";
|
||
regracadNovoEmpresaNome.textContent = "Empresa não encontrada no Questor.";
|
||
}
|
||
// O backend só busca/salva o código normalizado (sem zero à esquerda,
|
||
// ex. "092" -> "92") — reflete isso no campo assim que a resposta
|
||
// chega, pra nunca ficar exibindo/enviando um código "não-canônico".
|
||
if (resultado && resultado.codigo_empresa && resultado.codigo_empresa !== codigo) {
|
||
regracadNovoCodigoEmpresa.value = resultado.codigo_empresa;
|
||
}
|
||
regracadAtualizarNovoOperadoraDuplicada();
|
||
}
|
||
|
||
// Busca o nome assim que o colaborador para de digitar (debounce), sem
|
||
// precisar esperar ele clicar em outro campo — pedido explícito do
|
||
// usuário pra ficar "mais instantâneo". `blur` continua funcionando
|
||
// também (chama a mesma função direto, sem esperar o debounce).
|
||
let regracadNomeEmpresaDebounce = null;
|
||
function agendarAtualizarNomeEmpresaNovo() {
|
||
if (regracadNomeEmpresaDebounce) clearTimeout(regracadNomeEmpresaDebounce);
|
||
regracadNomeEmpresaDebounce = setTimeout(atualizarNomeEmpresaNovo, 350);
|
||
}
|
||
|
||
// Confere se a empresa (campo texto)+operadora (combobox) de "+ Nova
|
||
// regra" já têm uma regra cadastrada — avisa embaixo da Operadora sem
|
||
// bloquear "Criar regra" sozinho (o backend também recusa via
|
||
// unique_together, mas avisar antes evita a viagem de ida e volta).
|
||
function regracadAtualizarNovoOperadoraDuplicada() {
|
||
if (!regracadNovoOperadoraDuplicada) return;
|
||
const codigo = regracadNovoCodigoEmpresa ? regracadNovoCodigoEmpresa.value.trim() : "";
|
||
const duplicada =
|
||
!!codigo &&
|
||
!!regracadNovoOperadoraKey &&
|
||
regras.some((r) => r.codigo_empresa === codigo && r.operadora === regracadNovoOperadoraKey);
|
||
regracadNovoOperadoraDuplicada.hidden = !duplicada;
|
||
if (regracadCriarBtn) regracadCriarBtn.disabled = duplicada;
|
||
}
|
||
|
||
function regracadEntrarModoNovo() {
|
||
regracadModoNovo = true;
|
||
regracadRegraEmEdicao = null;
|
||
regracadNovoOperadoraKey = null;
|
||
if (regracadNovoCodigoEmpresa) regracadNovoCodigoEmpresa.value = regracadEmpresaAtual || "";
|
||
regracadNovoOperadoraOps.setItens(operadorasCache);
|
||
regracadNovoOperadoraOps.limpar();
|
||
limparCusteioForm();
|
||
if (regracadObservacoes) regracadObservacoes.value = "";
|
||
if (regracadToolbar) regracadToolbar.hidden = true;
|
||
if (regracadVazio) regracadVazio.hidden = true;
|
||
if (regracadNovoView) regracadNovoView.hidden = false;
|
||
if (regracadFormView) regracadFormView.hidden = false;
|
||
if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.hidden = false;
|
||
if (regracadSalvarBtn) regracadSalvarBtn.hidden = true;
|
||
if (regracadExcluirBtn) regracadExcluirBtn.hidden = true;
|
||
if (regracadCriarBtn) {
|
||
regracadCriarBtn.hidden = false;
|
||
regracadCriarBtn.disabled = false;
|
||
}
|
||
if (regracadError) regracadError.textContent = "";
|
||
regracadSetModo("Cadastrando regra nova", "ips-regracad-modo--novo");
|
||
regracadAtualizarNovoOperadoraDuplicada();
|
||
if (regracadEmpresaAtual) {
|
||
atualizarNomeEmpresaNovo();
|
||
} else {
|
||
limparNomeEmpresaNovo();
|
||
}
|
||
}
|
||
|
||
// "Cancelar" em "+ Nova regra" — volta pro topo (Empresa/Operadora) sem
|
||
// fechar o modal inteiro, diferente de "Fechar".
|
||
function regracadCancelarNovo() {
|
||
regracadModoNovo = false;
|
||
if (regracadNovoView) regracadNovoView.hidden = true;
|
||
if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.hidden = true;
|
||
regracadMostrarConformeSelecaoAtual();
|
||
}
|
||
|
||
async function abrirCadastroRegras() {
|
||
regracadModal.hidden = false;
|
||
if (regracadConfirmFecharModal) regracadConfirmFecharModal.hidden = true;
|
||
regracadEmpresaAtual = null;
|
||
regracadOperadoraAtual = null;
|
||
regracadRegraEmEdicao = null;
|
||
regracadModoNovo = false;
|
||
regracadEmpresaOps.limpar();
|
||
regracadOperadoraOps.setItens([]);
|
||
regracadOperadoraOps.limpar();
|
||
if (regracadOperadoraSearch) regracadOperadoraSearch.disabled = true;
|
||
if (regracadOperadoraToggleBtn) regracadOperadoraToggleBtn.disabled = true;
|
||
if (regracadNovaOperadoraBtn) regracadNovaOperadoraBtn.hidden = true;
|
||
limparNomeEmpresaNovo();
|
||
// Reseta a visualização já aqui, síncrono e antes de qualquer await —
|
||
// sem isso, o modal reabria mostrando por um instante o que estava na
|
||
// tela (ex. "Cadastrando regra nova") de antes de fechar, só voltando
|
||
// ao estado vazio depois que as buscas abaixo terminassem.
|
||
regracadMostrarVazio();
|
||
try {
|
||
operadorasCache = await pidFetchOperadorasPlanoSaude();
|
||
} catch (e) {
|
||
operadorasCache = [];
|
||
}
|
||
try {
|
||
regrasEmpresaCache = await pidFetchRegrasEmpresaPlanoSaude();
|
||
} catch (e) {
|
||
regrasEmpresaCache = [];
|
||
}
|
||
await refreshRegras();
|
||
regracadEmpresaOps.setItens(empresasComRegra());
|
||
regracadMostrarVazio();
|
||
}
|
||
|
||
function fecharCadastroRegras() {
|
||
regracadModal.hidden = true;
|
||
}
|
||
|
||
async function openNewForm() {
|
||
validadorPlanilha.limpar();
|
||
validadorArquivo.limpar();
|
||
formError.textContent = "";
|
||
regraResolvidaAtual = null;
|
||
renderResumoRegra(null);
|
||
impEmpresaOps.limpar();
|
||
impOperadoraOps.setItens([]);
|
||
impOperadoraOps.limpar();
|
||
if (impOperadoraSearch) impOperadoraSearch.disabled = true;
|
||
if (impOperadoraToggleBtn) impOperadoraToggleBtn.disabled = true;
|
||
try {
|
||
operadorasCache = await pidFetchOperadorasPlanoSaude();
|
||
} catch (e) {
|
||
operadorasCache = [];
|
||
}
|
||
try {
|
||
regrasEmpresaCache = await pidFetchRegrasEmpresaPlanoSaude();
|
||
} catch (e) {
|
||
regrasEmpresaCache = [];
|
||
}
|
||
await refreshRegras();
|
||
impEmpresaOps.setItens(empresasComRegra());
|
||
showView("form");
|
||
}
|
||
|
||
function abrirRevisao(importacao) {
|
||
importacaoAtual = importacao;
|
||
activeTab = null;
|
||
if (reviewEmpresa) {
|
||
if (importacao.codigo_empresa) {
|
||
reviewEmpresa.textContent = importacao.nome_empresa
|
||
? `— ${importacao.codigo_empresa} - ${importacao.nome_empresa}`
|
||
: `— ${importacao.codigo_empresa}`;
|
||
reviewEmpresa.hidden = false;
|
||
} else {
|
||
reviewEmpresa.hidden = true;
|
||
reviewEmpresa.textContent = "";
|
||
}
|
||
}
|
||
const regraEmpresaSufixo = importacao.regra_empresa_label ? ` — Regra empresa: ${importacao.regra_empresa_label}` : "";
|
||
reviewSubtitle.textContent =
|
||
`${importacao.nome_operadora} — criada em ${new Date(importacao.criado_em).toLocaleString("pt-BR")}${regraEmpresaSufixo}`;
|
||
if (reviewRegraEmpresaObs) {
|
||
// As duas regras (empresa e de custeio salva) são mutuamente exclusivas
|
||
// na criação — nunca as duas preenchidas ao mesmo tempo — mas o bloco é
|
||
// o mesmo pra qualquer uma que tenha observação cadastrada.
|
||
if (importacao.regra_empresa_observacoes) {
|
||
reviewRegraEmpresaObsLabel.textContent = "Observações da regra empresa";
|
||
reviewRegraEmpresaObsTexto.textContent = importacao.regra_empresa_observacoes;
|
||
reviewRegraEmpresaObs.hidden = false;
|
||
} else if (importacao.regra_custeio_salva_observacoes) {
|
||
const nome = importacao.regra_custeio_salva_nome ? ` — ${importacao.regra_custeio_salva_nome}` : "";
|
||
reviewRegraEmpresaObsLabel.textContent = `Observações da regra de custeio salva${nome}`;
|
||
reviewRegraEmpresaObsTexto.textContent = importacao.regra_custeio_salva_observacoes;
|
||
reviewRegraEmpresaObs.hidden = false;
|
||
} else {
|
||
reviewRegraEmpresaObs.hidden = true;
|
||
reviewRegraEmpresaObsTexto.textContent = "";
|
||
}
|
||
}
|
||
reviewError.textContent = "";
|
||
renderTabs();
|
||
showView("review");
|
||
}
|
||
|
||
function renderTabs() {
|
||
const tipos = importacaoAtual.tipos_lancamento || [];
|
||
const tabKeys = [...tipos, "auditoria", "alteracoes"];
|
||
if (!activeTab || !tabKeys.includes(activeTab)) activeTab = tabKeys[0];
|
||
|
||
tabsEl.innerHTML = tabKeys
|
||
.map((key) => {
|
||
const label = key === "auditoria" ? "Auditoria" : key === "alteracoes" ? "Alterações" : PID_IPS_TIPO_LABELS[key] || key;
|
||
return `<button type="button" class="pa-tab${key === activeTab ? " is-active" : ""}" data-ips-tab="${key}">${label}</button>`;
|
||
})
|
||
.join("");
|
||
|
||
renderPanels();
|
||
}
|
||
|
||
function escapeAttr(value) {
|
||
return (value === null || value === undefined ? "" : String(value)).replace(/"/g, """);
|
||
}
|
||
|
||
// 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>`;
|
||
}
|
||
|
||
// Aba "Alterações" — histórico de edição/inclusão/exclusão de linha feito
|
||
// manualmente na revisão (ImportacaoPlanoSaudeAlteracao, ver models.py). Já
|
||
// vem do backend ordenado do mais recente pro mais antigo; sem ordenação/
|
||
// redimensionamento de coluna (é um log, não uma planilha editável).
|
||
function panelHtmlAlteracoes() {
|
||
const alteracoes = importacaoAtual.alteracoes || [];
|
||
if (!alteracoes.length) {
|
||
return `<p class="ips-empty">Nenhuma alteração registrada nesta importação ainda.</p>`;
|
||
}
|
||
|
||
const linhasHtml = alteracoes
|
||
.map((alt) => {
|
||
let detalheHtml;
|
||
if (alt.tipo === "edicao") {
|
||
const campoLabel = PID_IPS_CAMPO_LABELS[alt.campo] || alt.campo;
|
||
detalheHtml = `<strong>${escapeHtml(campoLabel)}:</strong> "${escapeHtml(alt.valor_anterior)}" → "${escapeHtml(alt.valor_novo)}"`;
|
||
} else if (alt.tipo === "inclusao") {
|
||
detalheHtml = "Linha incluída manualmente";
|
||
} else {
|
||
detalheHtml = "Linha removida";
|
||
}
|
||
|
||
const acaoHtml = alt.revertida
|
||
? `<span class="ips-alteracao-revertida">Revertida</span>`
|
||
: `<button type="button" class="btn-outline ips-reverter-btn" data-reverter-alteracao="${alt.id}">Reverter</button>`;
|
||
|
||
return `
|
||
<tr>
|
||
<td class="ips-table-actions">${acaoHtml}</td>
|
||
<td>${new Date(alt.criado_em).toLocaleString("pt-BR")}</td>
|
||
<td><span class="ips-alteracao-tipo ips-alteracao-tipo--${alt.tipo}">${PID_IPS_ALTERACAO_TIPO_LABELS[alt.tipo] || alt.tipo}</span></td>
|
||
<td>${PID_IPS_TIPO_LABELS[alt.tipo_lancamento] || alt.tipo_lancamento || "—"}</td>
|
||
<td>${alt.linha_nome ? escapeHtml(alt.linha_nome) : "—"}</td>
|
||
<td>${detalheHtml}</td>
|
||
<td>${alt.usuario_nome ? escapeHtml(alt.usuario_nome) : "—"}</td>
|
||
</tr>`;
|
||
})
|
||
.join("");
|
||
|
||
return `
|
||
<div class="pa-table-wrap ips-table-scroll">
|
||
<table class="pa-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="ips-table-actions">Ação</th>
|
||
<th>Quando</th>
|
||
<th>Tipo</th>
|
||
<th>Lançamento</th>
|
||
<th>Linha</th>
|
||
<th>Alteração</th>
|
||
<th>Usuário</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${linhasHtml}</tbody>
|
||
</table>
|
||
</div>`;
|
||
}
|
||
|
||
function renderPanels() {
|
||
const tipos = importacaoAtual.tipos_lancamento || [];
|
||
panelsEl.innerHTML = [...tipos, "auditoria", "alteracoes"]
|
||
.map((key) => {
|
||
const html =
|
||
key === "auditoria" ? panelHtmlAuditoria() : key === "alteracoes" ? panelHtmlAlteracoes() : panelHtmlParaTipo(key);
|
||
return `<div class="pa-tab-panel" data-ips-tab-panel="${key}" ${key === activeTab ? "" : "hidden"}>${html}</div>`;
|
||
})
|
||
.join("");
|
||
wireColumnResize(panelsEl);
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Vincular pessoa (resolução manual de NOME_DIVERGENTE/NAO_CADASTRADO)
|
||
// ------------------------------------------------------------------
|
||
|
||
function linhaEhTitular(linha) {
|
||
return !(linha.nome_dependente || "").trim() && !(linha.cpf_dependente || "").trim();
|
||
}
|
||
|
||
function candidatosVincular(item) {
|
||
const ehTitular = item.tipo === "T";
|
||
return (importacaoAtual.linhas || []).filter(
|
||
(l) => l.tipo_lancamento === item.tipo_lancamento && l.valor === "0" && l.valor_empresa === "0" && linhaEhTitular(l) === ehTitular
|
||
);
|
||
}
|
||
|
||
function renderVincularLista(filtro) {
|
||
const termo = (filtro || "").trim().toLowerCase();
|
||
const candidatos = candidatosVincular(vincularItemAtual).filter((l) => {
|
||
const nomePrincipal = (l.nome_dependente || l.nome_func || "").toLowerCase();
|
||
const nomeTitular = (l.nome_func || "").toLowerCase();
|
||
return nomePrincipal.includes(termo) || nomeTitular.includes(termo);
|
||
});
|
||
|
||
if (!candidatos.length) {
|
||
vincularLista.innerHTML = `<p class="checklist-empty">Nenhuma linha em branco encontrada na planilha padrão.</p>`;
|
||
return;
|
||
}
|
||
|
||
vincularLista.innerHTML = candidatos
|
||
.map((l) => {
|
||
const nomePrincipal = l.nome_dependente || l.nome_func;
|
||
const contexto = l.nome_dependente ? `Titular: ${l.nome_func}` : "Titular";
|
||
return `
|
||
<label class="checklist-item">
|
||
<input type="radio" name="ips-vincular-linha" value="${l.id}" ${vincularLinhaSelecionada === l.id ? "checked" : ""} />
|
||
<span class="checklist-item__info">
|
||
<span class="checklist-item__nome">${nomePrincipal}</span>
|
||
<span class="checklist-item__departamento">${contexto}</span>
|
||
</span>
|
||
</label>`;
|
||
})
|
||
.join("");
|
||
}
|
||
|
||
function abrirVincularModal(item) {
|
||
vincularItemAtual = item;
|
||
vincularLinhaSelecionada = null;
|
||
vincularError.textContent = "";
|
||
vincularConfirmBtn.disabled = true;
|
||
vincularSearch.value = "";
|
||
vincularResumo.textContent = `"${item.nome}" (${item.tipo === "T" ? "Titular" : "Dependente"} — ${
|
||
PID_IPS_TIPO_LABELS[item.tipo_lancamento] || item.tipo_lancamento
|
||
}) não foi encontrado automaticamente. Selecione a pessoa correta na planilha padrão:`;
|
||
renderVincularLista("");
|
||
vincularModal.hidden = false;
|
||
}
|
||
|
||
function fecharVincularModal() {
|
||
vincularModal.hidden = true;
|
||
vincularItemAtual = null;
|
||
vincularLinhaSelecionada = null;
|
||
}
|
||
|
||
if (vincularModal) {
|
||
vincularModal.addEventListener("click", (event) => {
|
||
if (event.target === vincularModal) fecharVincularModal();
|
||
});
|
||
}
|
||
if (vincularCancelBtn) vincularCancelBtn.addEventListener("click", fecharVincularModal);
|
||
if (vincularSearch) {
|
||
vincularSearch.addEventListener("input", () => renderVincularLista(vincularSearch.value));
|
||
}
|
||
if (vincularLista) {
|
||
vincularLista.addEventListener("change", (event) => {
|
||
if (event.target.name !== "ips-vincular-linha") return;
|
||
vincularLinhaSelecionada = Number(event.target.value);
|
||
vincularConfirmBtn.disabled = false;
|
||
});
|
||
}
|
||
if (vincularConfirmBtn) {
|
||
vincularConfirmBtn.addEventListener("click", async () => {
|
||
if (!vincularItemAtual || !vincularLinhaSelecionada) return;
|
||
vincularError.textContent = "";
|
||
vincularConfirmBtn.disabled = true;
|
||
try {
|
||
await pidResolverAuditoriaPlanoSaude(vincularItemAtual.id, vincularLinhaSelecionada);
|
||
importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id);
|
||
fecharVincularModal();
|
||
renderTabs();
|
||
} catch (e) {
|
||
vincularError.textContent = e.message;
|
||
vincularConfirmBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (newBtn) newBtn.addEventListener("click", openNewForm);
|
||
if (formCancelBtn) formCancelBtn.addEventListener("click", () => showView("list"));
|
||
if (formBackBtn) formBackBtn.addEventListener("click", () => showView("list"));
|
||
if (reviewBackBtn) {
|
||
reviewBackBtn.addEventListener("click", async () => {
|
||
showView("list");
|
||
await refreshList();
|
||
});
|
||
}
|
||
|
||
if (regracadBtn) regracadBtn.addEventListener("click", abrirCadastroRegras);
|
||
if (regracadFecharBtn) regracadFecharBtn.addEventListener("click", fecharCadastroRegras);
|
||
if (regracadModal) {
|
||
regracadModal.addEventListener("click", (event) => {
|
||
// Diferente dos outros modais do app: este formulário costuma ter
|
||
// edição em andamento (custeio, observações) — clicar fora por
|
||
// engano não deve descartar isso sem perguntar. Usa um modal próprio
|
||
// (mesmo padrão visual de .modal-overlay/.modal-card do resto do
|
||
// app) em vez de window.confirm() — o diálogo nativo do browser
|
||
// aparece com a URL do servidor ("192.168.x.x:8000 diz..."), fora do
|
||
// visual do Portal.
|
||
if (event.target === regracadModal && regracadConfirmFecharModal) {
|
||
regracadConfirmFecharModal.hidden = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (regracadConfirmFecharCancelarBtn) {
|
||
regracadConfirmFecharCancelarBtn.addEventListener("click", () => {
|
||
regracadConfirmFecharModal.hidden = true;
|
||
});
|
||
}
|
||
if (regracadConfirmFecharOkBtn) {
|
||
regracadConfirmFecharOkBtn.addEventListener("click", () => {
|
||
regracadConfirmFecharModal.hidden = true;
|
||
fecharCadastroRegras();
|
||
});
|
||
}
|
||
if (regracadConfirmFecharModal) {
|
||
// Clicar fora do próprio diálogo de confirmação cancela (não fecha o
|
||
// cadastro) — mesmo espírito conservador do resto do app.
|
||
regracadConfirmFecharModal.addEventListener("click", (event) => {
|
||
if (event.target === regracadConfirmFecharModal) regracadConfirmFecharModal.hidden = true;
|
||
});
|
||
}
|
||
if (regracadNovaBtn) regracadNovaBtn.addEventListener("click", regracadEntrarModoNovo);
|
||
if (regracadNovaOperadoraBtn) regracadNovaOperadoraBtn.addEventListener("click", regracadEntrarModoNovo);
|
||
if (regracadNovoCancelarBtn) regracadNovoCancelarBtn.addEventListener("click", regracadCancelarNovo);
|
||
if (regracadNovoCodigoEmpresa) {
|
||
regracadNovoCodigoEmpresa.addEventListener("input", agendarAtualizarNomeEmpresaNovo);
|
||
regracadNovoCodigoEmpresa.addEventListener("blur", atualizarNomeEmpresaNovo);
|
||
}
|
||
|
||
document.addEventListener("click", (event) => {
|
||
[impEmpresaOps, impOperadoraOps, regracadEmpresaOps, regracadOperadoraOps, regracadNovoOperadoraOps].forEach(
|
||
(combo) => {
|
||
if (combo && !combo.contains(event.target)) combo.fechar();
|
||
}
|
||
);
|
||
});
|
||
|
||
if (formTipoMensalidade) {
|
||
formTipoMensalidade.addEventListener("change", () => {
|
||
custeioMensalidade.hidden = !formTipoMensalidade.checked;
|
||
// Mutuamente exclusivo com "Regra empresa" — as duas são formas
|
||
// alternativas de configurar o mesmo tipo de lançamento "mensalidade".
|
||
if (formTipoMensalidade.checked) limparRegraEmpresa();
|
||
});
|
||
}
|
||
if (formTipoCoparticipacao) {
|
||
formTipoCoparticipacao.addEventListener("change", () => {
|
||
custeioCoparticipacao.hidden = !formTipoCoparticipacao.checked;
|
||
});
|
||
}
|
||
if (formTipoRegraEmpresa) {
|
||
formTipoRegraEmpresa.addEventListener("change", () => {
|
||
if (regraEmpresaBox) regraEmpresaBox.hidden = !formTipoRegraEmpresa.checked;
|
||
if (formTipoRegraEmpresa.checked) {
|
||
// Mesma exclusividade mútua do outro lado (formTipoMensalidade acima).
|
||
formTipoMensalidade.checked = false;
|
||
custeioMensalidade.hidden = true;
|
||
custeioMensalidade.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false));
|
||
} else {
|
||
regraEmpresaSelecionada = null;
|
||
renderRegraEmpresaAtual();
|
||
}
|
||
});
|
||
}
|
||
if (regraEmpresaSelecionarBtn) {
|
||
regraEmpresaSelecionarBtn.addEventListener("click", () => {
|
||
if (regraEmpresaError) regraEmpresaError.textContent = "";
|
||
renderRegraEmpresaLista();
|
||
if (regraEmpresaModal) regraEmpresaModal.hidden = false;
|
||
});
|
||
}
|
||
if (regraEmpresaLista) {
|
||
regraEmpresaLista.addEventListener("click", (event) => {
|
||
const btn = event.target.closest("[data-regra-empresa-selecionar]");
|
||
if (!btn) return;
|
||
const key = btn.getAttribute("data-regra-empresa-selecionar");
|
||
const regra = regrasEmpresaCache.find((r) => r.key === key);
|
||
if (!regra) return;
|
||
regraEmpresaSelecionada = regra;
|
||
renderRegraEmpresaAtual();
|
||
if (regraEmpresaModal) regraEmpresaModal.hidden = true;
|
||
});
|
||
}
|
||
if (regraEmpresaCancelBtn) regraEmpresaCancelBtn.addEventListener("click", () => (regraEmpresaModal.hidden = true));
|
||
if (regraEmpresaModal) {
|
||
regraEmpresaModal.addEventListener("click", (event) => {
|
||
if (event.target === regraEmpresaModal) regraEmpresaModal.hidden = true;
|
||
});
|
||
}
|
||
|
||
if (regracadSalvarBtn) {
|
||
regracadSalvarBtn.addEventListener("click", async () => {
|
||
if (!regracadRegraEmEdicao) return;
|
||
const { tipos_lancamento, custeio_por_tipo } = coletarCusteioAtual();
|
||
const erro = mensagemErroCusteio(tipos_lancamento);
|
||
if (erro) {
|
||
regracadError.textContent = erro;
|
||
return;
|
||
}
|
||
const payload = {
|
||
codigo_empresa: regracadEmpresaAtual,
|
||
operadora: regracadOperadoraAtual,
|
||
regra_empresa_chave: formTipoRegraEmpresa.checked && regraEmpresaSelecionada ? regraEmpresaSelecionada.key : "",
|
||
tipos_lancamento,
|
||
custeio_por_tipo,
|
||
observacoes: regracadObservacoes ? regracadObservacoes.value.trim() : "",
|
||
};
|
||
regracadSalvarBtn.disabled = true;
|
||
try {
|
||
await pidAtualizarRegraCusteioPlanoSaude(regracadRegraEmEdicao.id, payload);
|
||
await refreshRegras();
|
||
// Pedido explícito do usuário: depois de salvar com sucesso, fecha
|
||
// o modal em vez de continuar mostrando a regra editada.
|
||
fecharCadastroRegras();
|
||
} catch (e) {
|
||
regracadError.textContent = e.message;
|
||
} finally {
|
||
regracadSalvarBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (regracadExcluirBtn) {
|
||
regracadExcluirBtn.addEventListener("click", async () => {
|
||
if (!regracadRegraEmEdicao) return;
|
||
if (!window.confirm(`Excluir a regra "${regracadRegraEmEdicao.nome}"? Essa ação não pode ser desfeita.`)) return;
|
||
regracadExcluirBtn.disabled = true;
|
||
try {
|
||
await pidExcluirRegraCusteioPlanoSaude(regracadRegraEmEdicao.id);
|
||
await refreshRegras();
|
||
regracadEmpresaOps.setItens(empresasComRegra());
|
||
regracadOperadoraOps.setItens(regracadEmpresaAtual ? operadorasComRegraParaEmpresa(regracadEmpresaAtual) : []);
|
||
regracadOperadoraOps.limpar();
|
||
regracadRegraEmEdicao = null;
|
||
regracadMostrarVazio();
|
||
} catch (e) {
|
||
regracadError.textContent = e.message;
|
||
} finally {
|
||
regracadExcluirBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (regracadCriarBtn) {
|
||
regracadCriarBtn.addEventListener("click", async () => {
|
||
const codigoEmpresa = regracadNovoCodigoEmpresa ? regracadNovoCodigoEmpresa.value.trim() : "";
|
||
if (!codigoEmpresa) {
|
||
regracadError.textContent = "Informe o código da empresa.";
|
||
return;
|
||
}
|
||
if (!regracadNovoOperadoraKey) {
|
||
regracadError.textContent = "Selecione a operadora.";
|
||
return;
|
||
}
|
||
const { tipos_lancamento, custeio_por_tipo } = coletarCusteioAtual();
|
||
const erro = mensagemErroCusteio(tipos_lancamento);
|
||
if (erro) {
|
||
regracadError.textContent = erro;
|
||
return;
|
||
}
|
||
const payload = {
|
||
codigo_empresa: codigoEmpresa,
|
||
operadora: regracadNovoOperadoraKey,
|
||
regra_empresa_chave: formTipoRegraEmpresa.checked && regraEmpresaSelecionada ? regraEmpresaSelecionada.key : "",
|
||
tipos_lancamento,
|
||
custeio_por_tipo,
|
||
observacoes: regracadObservacoes ? regracadObservacoes.value.trim() : "",
|
||
};
|
||
regracadCriarBtn.disabled = true;
|
||
try {
|
||
await pidCriarRegraCusteioPlanoSaude(payload);
|
||
await refreshRegras();
|
||
// Pedido explícito do usuário: depois de criar com sucesso, fecha
|
||
// o modal em vez de continuar mostrando a regra recém-criada.
|
||
fecharCadastroRegras();
|
||
} catch (e) {
|
||
regracadError.textContent = e.message;
|
||
} finally {
|
||
regracadCriarBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
PID_IPS_TIPOS.forEach((tipo) => {
|
||
const custeioBox = document.getElementById(`ips-custeio-${tipo}`);
|
||
if (!custeioBox) return;
|
||
custeioBox.addEventListener("change", (event) => {
|
||
const pessoa = PID_IPS_PESSOAS.find((p) => event.target.name === `ips-custeio-${tipo}-${p}`);
|
||
if (!pessoa) return;
|
||
const regraBox = document.getElementById(`ips-regra-especifica-${tipo}-${pessoa}`);
|
||
if (regraBox) regraBox.hidden = event.target.value !== "especifica";
|
||
});
|
||
});
|
||
|
||
if (tabsEl) {
|
||
tabsEl.addEventListener("click", (event) => {
|
||
const btn = event.target.closest("[data-ips-tab]");
|
||
if (!btn) return;
|
||
activeTab = btn.getAttribute("data-ips-tab");
|
||
renderTabs();
|
||
});
|
||
}
|
||
|
||
if (panelsEl) {
|
||
panelsEl.addEventListener("change", async (event) => {
|
||
const input = event.target.closest("[data-linha-campo]");
|
||
if (!input) return;
|
||
const row = input.closest("[data-linha-id]");
|
||
const linhaId = Number(row.getAttribute("data-linha-id"));
|
||
const campo = input.getAttribute("data-linha-campo");
|
||
try {
|
||
await pidAtualizarLinhaPlanoSaude(linhaId, { [campo]: input.value.trim() });
|
||
reviewError.textContent = "";
|
||
} catch (e) {
|
||
reviewError.textContent = e.message;
|
||
}
|
||
});
|
||
|
||
panelsEl.addEventListener("click", async (event) => {
|
||
if (!event.target.closest(".ips-col-resize-handle")) {
|
||
const sortTh = event.target.closest("[data-col-campo]");
|
||
if (sortTh) {
|
||
const campo = sortTh.getAttribute("data-col-campo");
|
||
if (sortTh.getAttribute("data-col-target") === "auditoria") {
|
||
if (auditoriaSortKey === campo) auditoriaSortDir *= -1;
|
||
else {
|
||
auditoriaSortKey = campo;
|
||
auditoriaSortDir = 1;
|
||
}
|
||
} else {
|
||
if (linhaSortKey === campo) linhaSortDir *= -1;
|
||
else {
|
||
linhaSortKey = campo;
|
||
linhaSortDir = 1;
|
||
}
|
||
}
|
||
renderPanels();
|
||
return;
|
||
}
|
||
}
|
||
|
||
const addBtn = event.target.closest("[data-ips-add-linha]");
|
||
if (addBtn) {
|
||
const tipo = addBtn.getAttribute("data-ips-add-linha");
|
||
addBtn.disabled = true;
|
||
try {
|
||
const novaLinha = await pidCriarLinhaPlanoSaude(importacaoAtual.id, tipo);
|
||
importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id);
|
||
reviewError.textContent = "";
|
||
renderTabs();
|
||
const novaRow = panelsEl.querySelector(`[data-linha-id="${novaLinha.id}"]`);
|
||
const primeiroInput = novaRow && novaRow.querySelector("[data-linha-campo]");
|
||
if (primeiroInput) {
|
||
primeiroInput.scrollIntoView({ block: "nearest" });
|
||
primeiroInput.focus();
|
||
}
|
||
} catch (e) {
|
||
reviewError.textContent = e.message;
|
||
addBtn.disabled = false;
|
||
}
|
||
return;
|
||
}
|
||
|
||
const removeBtn = event.target.closest("[data-remover-linha]");
|
||
if (removeBtn) {
|
||
const linhaId = Number(removeBtn.getAttribute("data-remover-linha"));
|
||
if (!window.confirm("Remover esta linha? Ela não vai aparecer no arquivo gerado.")) return;
|
||
removeBtn.disabled = true;
|
||
try {
|
||
await pidExcluirLinhaPlanoSaude(linhaId);
|
||
importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id);
|
||
reviewError.textContent = "";
|
||
renderTabs();
|
||
} catch (e) {
|
||
reviewError.textContent = e.message;
|
||
removeBtn.disabled = false;
|
||
}
|
||
return;
|
||
}
|
||
|
||
const vincularBtn = event.target.closest("[data-vincular-auditoria]");
|
||
if (vincularBtn) {
|
||
const itemId = Number(vincularBtn.getAttribute("data-vincular-auditoria"));
|
||
const item = (importacaoAtual.itens_auditoria || []).find((i) => i.id === itemId);
|
||
if (item) abrirVincularModal(item);
|
||
return;
|
||
}
|
||
|
||
const reverterBtn = event.target.closest("[data-reverter-alteracao]");
|
||
if (reverterBtn) {
|
||
const alteracaoId = Number(reverterBtn.getAttribute("data-reverter-alteracao"));
|
||
if (!window.confirm("Reverter esta alteração?")) return;
|
||
reverterBtn.disabled = true;
|
||
try {
|
||
await pidReverterAlteracaoPlanoSaude(alteracaoId);
|
||
importacaoAtual = await pidFetchImportacaoPlanoSaude(importacaoAtual.id);
|
||
reviewError.textContent = "";
|
||
renderTabs();
|
||
} catch (e) {
|
||
reviewError.textContent = e.message;
|
||
reverterBtn.disabled = false;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
if (gerarBtn) {
|
||
gerarBtn.addEventListener("click", async () => {
|
||
reviewError.textContent = "";
|
||
gerarBtn.disabled = true;
|
||
try {
|
||
await pidGerarArquivoPlanoSaude(importacaoAtual.id);
|
||
} 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 () => {
|
||
if (!regraResolvidaAtual) {
|
||
formError.textContent = "Selecione a empresa e 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;
|
||
}
|
||
formError.textContent = "";
|
||
|
||
const formData = new FormData();
|
||
formData.append("planilha_padrao", formPlanilha.files[0]);
|
||
formData.append("arquivo_operadora", formArquivo.files[0]);
|
||
montarFormDataDeRegra(formData, regraResolvidaAtual);
|
||
|
||
formSubmitBtn.disabled = true;
|
||
formSubmitBtn.textContent = "Processando...";
|
||
try {
|
||
const importacao = await pidCriarImportacaoPlanoSaude(formData);
|
||
abrirRevisao(importacao);
|
||
await refreshList();
|
||
} catch (e) {
|
||
formError.textContent = e.message;
|
||
} finally {
|
||
formSubmitBtn.disabled = false;
|
||
formSubmitBtn.textContent = "Processar";
|
||
}
|
||
});
|
||
}
|
||
|
||
await refreshList();
|
||
});
|