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 = '' + '' + ''; 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 = '' + '' + '' + ''; 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(); });