451 lines
19 KiB
JavaScript
451 lines
19 KiB
JavaScript
document.addEventListener("DOMContentLoaded", async () => {
|
|
const formEl = document.getElementById("cc-form");
|
|
if (!formEl) return;
|
|
|
|
const me = await pidGetMe();
|
|
if (!me) return;
|
|
|
|
const geradocPerms = (me.permissoes_efetivas && me.permissoes_efetivas["geradoc"]) || {};
|
|
const canUse = !!(geradocPerms.apps && geradocPerms.apps["simulacao-custo-contratacao"]);
|
|
|
|
const noAccessEl = document.getElementById("cc-no-access");
|
|
const contentEl = document.getElementById("cc-content");
|
|
if (noAccessEl) noAccessEl.hidden = canUse;
|
|
if (contentEl) contentEl.hidden = !canUse;
|
|
if (!canUse) return;
|
|
|
|
const errorEl = document.getElementById("cc-form-error");
|
|
const submitBtn = document.getElementById("cc-form-submit-btn");
|
|
const limparBtn = document.getElementById("cc-form-limpar-btn");
|
|
|
|
const campoValor = (id) => document.getElementById(id).value.trim();
|
|
|
|
function parseValorBr(texto) {
|
|
if (!texto) return 0;
|
|
const numero = parseFloat(String(texto).trim().replace(/\./g, "").replace(",", "."));
|
|
return isNaN(numero) ? 0 : numero;
|
|
}
|
|
|
|
/** Número no formato brasileiro ("1.234,56", "7,5", "1234") ou `null` se
|
|
* o texto não for um número. Usado nas tabelas fiscais, onde um valor
|
|
* inválido virava 0 sem aviso e era gravado para todos (revisão de
|
|
* 30/09/2026). */
|
|
function parseValorBrEstrito(texto) {
|
|
const limpo = String(texto || "").trim();
|
|
if (!/^-?(\d{1,3}(\.\d{3})+|\d+)(,\d+)?$/.test(limpo)) return null;
|
|
return parseFloat(limpo.replace(/\./g, "").replace(",", "."));
|
|
}
|
|
|
|
// Sem separador de milhar: usada pra montar o payload (percentual
|
|
// convertido em valor), que o servidor lê como número simples.
|
|
function formataValorBr(numero) {
|
|
return numero.toFixed(2).replace(".", ",");
|
|
}
|
|
|
|
// Com separador de milhar ("7.350,00"), só pra preencher os inputs das
|
|
// tabelas fiscais: parseValorBrEstrito aceita as duas formas
|
|
// ("1.234,56" e "1234,56") ao salvar.
|
|
const PID_CC_VALOR_EXIBICAO = new Intl.NumberFormat("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
function formataValorBrMilhar(numero) {
|
|
return PID_CC_VALOR_EXIBICAO.format(Number(numero) || 0);
|
|
}
|
|
|
|
// Coeficiente B da Lei 15.270/2025 tem muitas casas e vem do banco com
|
|
// ponto ("0.133145"): exibido com vírgula, sem arredondar e sem milhar.
|
|
// O salvar aceita vírgula ou ponto.
|
|
function formataCoeficienteBr(numero) {
|
|
if (numero === null || numero === undefined || numero === "") return "";
|
|
return String(numero).replace(".", ",");
|
|
}
|
|
|
|
/**
|
|
* Converte um texto que pode ser um valor em reais (ex.: "3.400,00") ou um
|
|
* percentual do salário contratual (ex.: "40%") pro valor em reais final —
|
|
* usado nos campos que aceitam as duas formas (Adicional Art. 62. e os
|
|
* campos da seção Descontos). Se o texto terminar em "%", calcula o valor
|
|
* a partir do salário antes de enviar pra API, que sempre espera um valor
|
|
* monetário nesses campos.
|
|
*/
|
|
function converteValorOuPercentual(bruto) {
|
|
if (!bruto) return "0";
|
|
if (bruto.endsWith("%")) {
|
|
const percentual = parseValorBr(bruto.slice(0, -1));
|
|
const salario = parseValorBr(campoValor("cc-salario"));
|
|
return formataValorBr((percentual / 100) * salario);
|
|
}
|
|
return bruto;
|
|
}
|
|
|
|
function valorOuPercentualDoSalario(id) {
|
|
return converteValorOuPercentual(campoValor(id));
|
|
}
|
|
|
|
function criarLinhaItem(nome, valor) {
|
|
const tr = document.createElement("tr");
|
|
tr.innerHTML =
|
|
'<td><input type="text" class="cc-tabela-fiscal__input" data-campo="nome" autocomplete="off" /></td>' +
|
|
'<td><input type="text" class="cc-tabela-fiscal__input" data-campo="valor" autocomplete="off" /></td>' +
|
|
'<td><button type="button" class="icon-btn cc-tabela-fiscal__remove" aria-label="Remover" title="Remover">✕</button></td>';
|
|
tr.querySelector('[data-campo="nome"]').value = nome || "";
|
|
tr.querySelector('[data-campo="valor"]').value = valor || "";
|
|
tr.querySelector(".cc-tabela-fiscal__remove").addEventListener("click", () => tr.remove());
|
|
return tr;
|
|
}
|
|
|
|
function lerItensDoTbody(tbodyEl, { comPercentual } = {}) {
|
|
return Array.from(tbodyEl.children)
|
|
.map((tr) => {
|
|
const nome = tr.querySelector('[data-campo="nome"]').value.trim();
|
|
const valorBruto = tr.querySelector('[data-campo="valor"]').value.trim();
|
|
return { nome, valor: comPercentual ? converteValorOuPercentual(valorBruto) : valorBruto || "0" };
|
|
})
|
|
.filter((item) => item.nome);
|
|
}
|
|
|
|
const remuneracaoOutrosBody = document.getElementById("cc-remuneracao-outros-body");
|
|
const beneficiosOutrosBody = document.getElementById("cc-beneficios-outros-body");
|
|
const descontosOutrosBody = document.getElementById("cc-descontos-outros-body");
|
|
|
|
const remuneracaoAddBtn = document.getElementById("cc-remuneracao-outros-add-btn");
|
|
if (remuneracaoAddBtn) {
|
|
remuneracaoAddBtn.addEventListener("click", () => {
|
|
remuneracaoOutrosBody.appendChild(criarLinhaItem());
|
|
});
|
|
}
|
|
|
|
const beneficiosAddBtn = document.getElementById("cc-beneficios-outros-add-btn");
|
|
if (beneficiosAddBtn) {
|
|
beneficiosAddBtn.addEventListener("click", () => {
|
|
beneficiosOutrosBody.appendChild(criarLinhaItem());
|
|
});
|
|
}
|
|
|
|
const descontosAddBtn = document.getElementById("cc-descontos-outros-add-btn");
|
|
if (descontosAddBtn) {
|
|
descontosAddBtn.addEventListener("click", () => {
|
|
descontosOutrosBody.appendChild(criarLinhaItem());
|
|
});
|
|
}
|
|
|
|
function montaPayload() {
|
|
return {
|
|
nome_referencia: campoValor("cc-nome-referencia"),
|
|
empresa: campoValor("cc-empresa"),
|
|
cargo: campoValor("cc-cargo"),
|
|
salario_contratual: campoValor("cc-salario"),
|
|
adicional_art62: valorOuPercentualDoSalario("cc-adicional"),
|
|
comissao: campoValor("cc-comissao") || "0",
|
|
dsr: campoValor("cc-dsr") || "0",
|
|
outros_itens_remuneracao: lerItensDoTbody(remuneracaoOutrosBody),
|
|
percentual_inss_empregador: campoValor("cc-perc-inss"),
|
|
percentual_fgts: campoValor("cc-perc-fgts"),
|
|
dependentes: Number(campoValor("cc-dependentes")) || 0,
|
|
vale_transporte: campoValor("cc-vale-transporte") || "0",
|
|
seguro_vida: campoValor("cc-seguro-vida") || "0",
|
|
vale_alimentacao: campoValor("cc-vale-alimentacao") || "0",
|
|
assiduidade: campoValor("cc-assiduidade") || "0",
|
|
fundo_formacao: campoValor("cc-fundo-formacao") || "0",
|
|
outros_beneficios: lerItensDoTbody(beneficiosOutrosBody),
|
|
desconto_vale_transporte: valorOuPercentualDoSalario("cc-desconto-vale-transporte"),
|
|
desconto_vale_alimentacao: valorOuPercentualDoSalario("cc-desconto-vale-alimentacao"),
|
|
desconto_sindicato: valorOuPercentualDoSalario("cc-desconto-sindicato"),
|
|
outros_descontos: lerItensDoTbody(descontosOutrosBody, { comPercentual: true }),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A resposta é o PDF binário, não JSON — por isso não passa por
|
|
* pidApiRequest. Reaproveita pidEnsureCsrfCookie/pidGetCookie/pidErrorMessageFrom
|
|
* de api.js (mesmo padrão de pidGerarArquivoPlanoSaude em
|
|
* importacao-plano-saude.js), mas abre o resultado numa aba nova em vez de
|
|
* forçar download — a ideia é já deixar o PDF pronto pra visualizar/imprimir/enviar.
|
|
*/
|
|
async function geraPdfSimulacao(payload, janela) {
|
|
await pidEnsureCsrfCookie();
|
|
const response = await fetch("/api/simulacao-custo-contratacao/gerar/", {
|
|
method: "POST",
|
|
credentials: "include",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-CSRFToken": pidGetCookie("csrftoken") || "",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let mensagem = "Não foi possível gerar o PDF.";
|
|
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 url = URL.createObjectURL(blob);
|
|
// A aba foi aberta no clique (antes de qualquer await), senão o
|
|
// bloqueador de pop-up costuma barrar; sem ela, tenta abrir agora.
|
|
if (janela && !janela.closed) janela.location.href = url;
|
|
else window.open(url, "_blank");
|
|
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
}
|
|
|
|
/** Linha de item com valor preenchido e sem nome: antes era descartada do
|
|
* cálculo sem aviso. Devolve o nome da seção com problema, ou "". */
|
|
function secaoComItemSemNome() {
|
|
const secoes = [
|
|
[remuneracaoOutrosBody, "Outros itens de remuneração"],
|
|
[beneficiosOutrosBody, "Outros benefícios"],
|
|
[descontosOutrosBody, "Outros descontos"],
|
|
];
|
|
for (const [tbody, rotulo] of secoes) {
|
|
const semNome = Array.from(tbody.children).find(
|
|
(tr) => !tr.querySelector('[data-campo="nome"]').value.trim() && tr.querySelector('[data-campo="valor"]').value.trim()
|
|
);
|
|
if (semNome) {
|
|
semNome.querySelector('[data-campo="nome"]').focus();
|
|
return rotulo;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// Leva o cursor ao campo apontado por uma mensagem de validação (a
|
|
// mensagem fica no rodapé do formulário, longe do campo).
|
|
function focarCampo(id) {
|
|
const campo = document.getElementById(id);
|
|
if (!campo) return;
|
|
campo.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
campo.focus({ preventScroll: true });
|
|
}
|
|
|
|
// Há algo preenchido no formulário da simulação (campo com valor diferente
|
|
// do original do HTML, ou linha incluída em "Outros itens"/"Outros
|
|
// benefícios"/"Outros descontos")? Só nesse caso "Limpar" pergunta antes.
|
|
function formularioTemConteudo() {
|
|
const campoAlterado = Array.from(formEl.querySelectorAll("input, select, textarea")).some((el) => {
|
|
if (el.type === "checkbox" || el.type === "radio") return el.checked !== el.defaultChecked;
|
|
return el.value.trim() !== (el.defaultValue || "").trim();
|
|
});
|
|
return (
|
|
campoAlterado ||
|
|
remuneracaoOutrosBody.children.length > 0 ||
|
|
beneficiosOutrosBody.children.length > 0 ||
|
|
descontosOutrosBody.children.length > 0
|
|
);
|
|
}
|
|
|
|
formEl.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
errorEl.textContent = "";
|
|
|
|
const payload = montaPayload();
|
|
if (!payload.salario_contratual) {
|
|
errorEl.textContent = "Informe o salário contratual.";
|
|
focarCampo("cc-salario");
|
|
return;
|
|
}
|
|
if (!payload.percentual_inss_empregador || !payload.percentual_fgts) {
|
|
errorEl.textContent = "Informe o % de INSS Empregador e o % de FGTS.";
|
|
focarCampo(payload.percentual_inss_empregador ? "cc-perc-fgts" : "cc-perc-inss");
|
|
return;
|
|
}
|
|
const secaoSemNome = secaoComItemSemNome();
|
|
if (secaoSemNome) {
|
|
errorEl.textContent = `Em "${secaoSemNome}", há um item com valor e sem nome. Informe o nome ou remova a linha.`;
|
|
return;
|
|
}
|
|
|
|
const janela = window.open("", "_blank");
|
|
submitBtn.disabled = true;
|
|
submitBtn.textContent = "Gerando…";
|
|
try {
|
|
await geraPdfSimulacao(payload, janela);
|
|
} catch (e) {
|
|
if (janela) janela.close();
|
|
errorEl.textContent = e.message;
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
submitBtn.textContent = "Gerar PDF";
|
|
}
|
|
});
|
|
|
|
if (limparBtn) {
|
|
limparBtn.addEventListener("click", async () => {
|
|
if (
|
|
formularioTemConteudo() &&
|
|
!(await pidConfirm("Limpar todos os campos da simulação? O que foi preenchido será perdido.", {
|
|
titulo: "Limpar simulação",
|
|
textoConfirmar: "Limpar",
|
|
perigoso: true,
|
|
}))
|
|
) {
|
|
return;
|
|
}
|
|
formEl.reset();
|
|
remuneracaoOutrosBody.innerHTML = "";
|
|
beneficiosOutrosBody.innerHTML = "";
|
|
descontosOutrosBody.innerHTML = "";
|
|
errorEl.textContent = "";
|
|
});
|
|
}
|
|
|
|
// ---- Painel "Ver/editar tabelas de INSS e IRRF" ----
|
|
const painelToggle = document.getElementById("cc-painel-fiscal-toggle");
|
|
const painelBody = document.getElementById("cc-painel-fiscal-body");
|
|
const painelError = document.getElementById("cc-painel-fiscal-error");
|
|
const painelStatus = document.getElementById("cc-painel-fiscal-status");
|
|
const painelSalvarBtn = document.getElementById("cc-painel-fiscal-salvar-btn");
|
|
const inssBody = document.getElementById("cc-tabela-inss-body");
|
|
const irrfBody = document.getElementById("cc-tabela-irrf-body");
|
|
|
|
if (painelToggle) {
|
|
painelToggle.addEventListener("click", () => {
|
|
const expandido = painelToggle.getAttribute("aria-expanded") === "true";
|
|
painelToggle.setAttribute("aria-expanded", String(!expandido));
|
|
painelBody.hidden = expandido;
|
|
});
|
|
}
|
|
|
|
function criarLinhaFaixa(faixa) {
|
|
const tr = document.createElement("tr");
|
|
const limite = formataValorBrMilhar(faixa.limite_superior);
|
|
const aliquota = formataValorBrMilhar(faixa.aliquota * 100);
|
|
const deduzir = formataValorBrMilhar(faixa.deduzir);
|
|
tr.innerHTML =
|
|
'<td><input type="text" class="cc-tabela-fiscal__input" data-campo="limite_superior" inputmode="decimal" autocomplete="off" /></td>' +
|
|
'<td><input type="text" class="cc-tabela-fiscal__input" data-campo="aliquota" inputmode="decimal" autocomplete="off" /></td>' +
|
|
'<td><input type="text" class="cc-tabela-fiscal__input" data-campo="deduzir" inputmode="decimal" autocomplete="off" /></td>' +
|
|
'<td><button type="button" class="icon-btn cc-tabela-fiscal__remove" aria-label="Remover faixa" title="Remover faixa">✕</button></td>';
|
|
tr.querySelector('[data-campo="limite_superior"]').value = limite;
|
|
tr.querySelector('[data-campo="aliquota"]').value = aliquota;
|
|
tr.querySelector('[data-campo="deduzir"]').value = deduzir;
|
|
tr.querySelector(".cc-tabela-fiscal__remove").addEventListener("click", () => tr.remove());
|
|
return tr;
|
|
}
|
|
|
|
function renderFaixas(tbodyEl, faixas) {
|
|
tbodyEl.innerHTML = "";
|
|
faixas.forEach((faixa) => tbodyEl.appendChild(criarLinhaFaixa(faixa)));
|
|
}
|
|
|
|
// Campos inválidos da última tentativa de salvar (marcados em vermelho).
|
|
let camposInvalidos = [];
|
|
|
|
/** Lê um campo numérico das tabelas fiscais; inválido entra na lista de
|
|
* erros (e fica marcado) em vez de virar 0. */
|
|
function lerNumeroFiscal(input) {
|
|
const numero = parseValorBrEstrito(input.value);
|
|
input.classList.toggle("is-invalido", numero === null);
|
|
if (numero === null) camposInvalidos.push(input);
|
|
return numero === null ? 0 : numero;
|
|
}
|
|
|
|
function lerFaixasDoTbody(tbodyEl) {
|
|
return Array.from(tbodyEl.children).map((tr) => {
|
|
const valores = {};
|
|
tr.querySelectorAll("input[data-campo]").forEach((input) => {
|
|
valores[input.dataset.campo] = lerNumeroFiscal(input);
|
|
});
|
|
return {
|
|
limite_superior: valores.limite_superior,
|
|
aliquota: valores.aliquota / 100,
|
|
deduzir: valores.deduzir,
|
|
};
|
|
});
|
|
}
|
|
|
|
// "Salvar tabelas" fica desabilitado até a carga terminar com sucesso:
|
|
// salvar com os campos ainda vazios (carga em andamento ou falha) gravaria
|
|
// tabelas zeradas pra todos.
|
|
async function carregarParametrosFiscais() {
|
|
if (painelSalvarBtn) painelSalvarBtn.disabled = true;
|
|
try {
|
|
const parametros = await pidApiRequest("/parametros-fiscais-custo-contratacao/");
|
|
renderFaixas(inssBody, parametros.faixas_inss);
|
|
renderFaixas(irrfBody, parametros.faixas_irrf);
|
|
document.getElementById("cc-inss-teto").value = formataValorBrMilhar(parametros.teto_desconto_inss);
|
|
document.getElementById("cc-irrf-topo-aliquota").value = formataValorBrMilhar(parametros.aliquota_irrf_topo * 100);
|
|
document.getElementById("cc-irrf-topo-deduzir").value = formataValorBrMilhar(parametros.deduzir_irrf_topo);
|
|
document.getElementById("cc-desconto-simplificado").value = formataValorBrMilhar(parametros.desconto_simplificado_irrf);
|
|
document.getElementById("cc-deducao-dependente").value = formataValorBrMilhar(parametros.deducao_por_dependente);
|
|
document.getElementById("cc-lei15270-a").value = formataValorBrMilhar(parametros.reducao_lei_15270_coeficiente_a);
|
|
document.getElementById("cc-lei15270-b").value = formataCoeficienteBr(parametros.reducao_lei_15270_coeficiente_b);
|
|
document.getElementById("cc-lei15270-limite").value = formataValorBrMilhar(parametros.reducao_lei_15270_limite);
|
|
if (painelSalvarBtn) painelSalvarBtn.disabled = false;
|
|
} catch (e) {
|
|
painelError.textContent =
|
|
"Não foi possível carregar as tabelas fiscais. Recarregue a página; se persistir, contate a Inovação. O salvar fica desabilitado até as tabelas carregarem.";
|
|
}
|
|
}
|
|
|
|
const inssAddBtn = document.getElementById("cc-inss-add-btn");
|
|
if (inssAddBtn) {
|
|
inssAddBtn.addEventListener("click", () => {
|
|
inssBody.appendChild(criarLinhaFaixa({ limite_superior: 0, aliquota: 0, deduzir: 0 }));
|
|
});
|
|
}
|
|
|
|
const irrfAddBtn = document.getElementById("cc-irrf-add-btn");
|
|
if (irrfAddBtn) {
|
|
irrfAddBtn.addEventListener("click", () => {
|
|
irrfBody.appendChild(criarLinhaFaixa({ limite_superior: 0, aliquota: 0, deduzir: 0 }));
|
|
});
|
|
}
|
|
|
|
if (painelSalvarBtn) {
|
|
painelSalvarBtn.addEventListener("click", async () => {
|
|
painelError.textContent = "";
|
|
painelStatus.hidden = true;
|
|
camposInvalidos = [];
|
|
const campo = (id) => lerNumeroFiscal(document.getElementById(id));
|
|
// Coeficiente B tem muitas casas: exibido com vírgula ("0,133145", ver
|
|
// formataCoeficienteBr), aceita ponto ou vírgula decimal, sem
|
|
// separador de milhar.
|
|
const coeficienteBInput = document.getElementById("cc-lei15270-b");
|
|
const coeficienteB = /^\d+([.,]\d+)?$/.test(coeficienteBInput.value.trim())
|
|
? parseFloat(coeficienteBInput.value.trim().replace(",", "."))
|
|
: null;
|
|
coeficienteBInput.classList.toggle("is-invalido", coeficienteB === null);
|
|
if (coeficienteB === null) camposInvalidos.push(coeficienteBInput);
|
|
const payload = {
|
|
faixas_inss: lerFaixasDoTbody(inssBody),
|
|
teto_desconto_inss: campo("cc-inss-teto"),
|
|
faixas_irrf: lerFaixasDoTbody(irrfBody),
|
|
aliquota_irrf_topo: campo("cc-irrf-topo-aliquota") / 100,
|
|
deduzir_irrf_topo: campo("cc-irrf-topo-deduzir"),
|
|
desconto_simplificado_irrf: campo("cc-desconto-simplificado"),
|
|
deducao_por_dependente: campo("cc-deducao-dependente"),
|
|
reducao_lei_15270_coeficiente_a: campo("cc-lei15270-a"),
|
|
reducao_lei_15270_coeficiente_b: coeficienteB || 0,
|
|
reducao_lei_15270_limite: campo("cc-lei15270-limite"),
|
|
};
|
|
if (camposInvalidos.length) {
|
|
painelError.textContent = `${camposInvalidos.length} campo(s) com valor inválido, marcados em vermelho. Use números no formato 1.234,56; nada foi salvo.`;
|
|
camposInvalidos[0].focus();
|
|
return;
|
|
}
|
|
painelSalvarBtn.disabled = true;
|
|
painelSalvarBtn.textContent = "Salvando…";
|
|
try {
|
|
const parametros = await pidApiRequest("/parametros-fiscais-custo-contratacao/", {
|
|
method: "PATCH",
|
|
body: payload,
|
|
});
|
|
renderFaixas(inssBody, parametros.faixas_inss);
|
|
renderFaixas(irrfBody, parametros.faixas_irrf);
|
|
painelStatus.textContent = "Tabelas salvas com sucesso.";
|
|
painelStatus.hidden = false;
|
|
} catch (e) {
|
|
painelError.textContent = e.message;
|
|
} finally {
|
|
painelSalvarBtn.disabled = false;
|
|
painelSalvarBtn.textContent = "Salvar tabelas";
|
|
}
|
|
});
|
|
}
|
|
|
|
carregarParametrosFiscais();
|
|
});
|