- Híbrido Forma de contratação: CLT Idioma: Inglês básico Híbrido — 3x por semana presencial - Faria Lima Quero me candidatarVer outras vagas Candidatura Envie a sua candidatura Anexar Currículo*Anexar CurrículoPDF — até 5MB Nome Completo* Seu melhor e-mail* Celular* País*Selecione…BrasilArgentinaChileColômbiaMéxicoPeruPortugalUruguaiEstados UnidosOutro Estado*UFACALAPAMBACEDFESGOMAMTMSMGPAPBPRPEPIRJRNRSRORRSCSPSETO Cidade* LinkedIn* Última Remuneração*PJCLT Autorizo o compartilhamento dos meus dados pessoais para fins de recrutamento e seleção, conforme a Política de Privacidade.
- Candidatar-se para a vaga Candidatura enviada!
- Recebemos a sua candidatura com sucesso.
- Nossa equipe entrará em contato em breve.
- Obrigado pelo interesse. (function () { 'use strict' var form = document.getElementById('candidatura') if (!form) return // Mensagens localizadas (injetadas no data-i18n do <form>) + endpoint (= action). var I18N = {} try { I18N = JSON.parse(form.getAttribute('data-i18n') || '{}') } catch (e) { I18N = {} } function msg(key) { return I18N[key] || '' } var apiUrl = form.getAttribute('action') || '/carreiras/api/candidatura' var municipiosUrl = form.getAttribute('data-municipios-url') || '/carreiras/api/municipios' var MAX_BYTES = 5 * 1024 * 1024 // 5MB var ALLOWED_EXT = ['pdf'] // Bitrix anexa só PDF — espelha o servidor. // Exige TLD com 2+ letras: reprova "teste@gmail" e "teste@" (que o /.+@.+\..+/ // clássico deixaria passar em parte dos casos). var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[A-Za-z]{2,}$/ // Celular BR já mascarado: (11) 98765-4321 — DDD + 9 dígitos começando em 9. var CELULAR_BR_RE = /^\(\d{2}\) 9\d{4}-\d{4}$/ var DDI_BRASIL = '+55' var PAIS_BRASIL = 'Brasil' var fileInput = form.querySelector('#curriculo') var fileNameEl = form.querySelector('[data-file-name]') var emailInput = form.querySelector('#email') var celularInput = form.querySelector('#celular') var ddiInput = form.querySelector('[data-ddi]') var paisSelect = form.querySelector('#paisOrigem') var estadoUf = form.querySelector('[data-estado-uf]') var estadoLivre = form.querySelector('[data-estado-livre]') var estadoLabel = form.querySelector('[data-estado-label]') var cidadeInput = form.querySelector('[data-cidade]') var cidadesLista = form.querySelector('[data-cidades-lista]') var linkedinInput = form.querySelector('#linkedin') var moedaInput = form.querySelector('[data-moeda]') var submitBtn = form.querySelector('[data-submit]') var aceiteInput = form.querySelector('#privacidade') var submitLabel = form.querySelector('[data-submit-label]') var statusEl = form.querySelector('[data-form-status]') var successPanel = form.parentNode ? form.parentNode.querySelector('[data-success]') : null var submitDefaultLabel = submitLabel ? (submitLabel.getAttribute('data-submit-default') || submitLabel.textContent || msg('enviar') || 'Enviar') : (msg('enviar') || 'Enviar') // ── Helpers de erro por campo ────────────────────────────── function fieldEl(name) { return form.querySelector('[data-field="' + name + '"]') } function setError(name, message) { var f = fieldEl(name) if (!f) return f.classList.add('has-error') var msg = f.querySelector('[data-error]') if (msg) { msg.textContent = message msg.classList.remove('hidden') } } function clearError(name) { var f = fieldEl(name) if (!f) return f.classList.remove('has-error') var msg = f.querySelector('[data-error]') if (msg) { msg.textContent = '' msg.classList.add('hidden') } } function clearAllErrors() { var fields = form.querySelectorAll('.field')
- for (var i = 0
- i < fields.length
fields[i].classList.remove('has-error') var msg = fields[i].querySelector('[data-error]') if (msg) { msg.textContent = '' msg.classList.add('hidden') } } } function setStatus(message, kind) { if (!statusEl) return statusEl.textContent = message || '' statusEl.classList.remove('hidden', 'text-brand-700', 'text-text-body') if (!message) { statusEl.classList.add('hidden') return } statusEl.classList.add(kind === 'error' ? 'text-brand-700' : 'text-text-body') } // ── Nome do arquivo escolhido ────────────────────────────── if (fileInput && fileNameEl) { var defaultFileText = fileNameEl.textContent fileInput.addEventListener('change', function () { if (fileInput.files && fileInput.files.length > 0) { fileNameEl.textContent = fileInput.files[0].name fileNameEl.classList.remove('text-text-muted') fileNameEl.classList.add('text-text-dark', 'font-semibold') } else { fileNameEl.textContent = defaultFileText fileNameEl.classList.add('text-text-muted') fileNameEl.classList.remove('text-text-dark', 'font-semibold') } clearError('curriculo') }) } // ── Máscaras ─────────────────────────────────────────────── // Conta caractere a caractere de propósito: `match` com regex sem `/g` pararia // na primeira ocorrência (era esse o bug que empurrava os dígitos para a // frente), e pôr `/g` na regex quebraria o `test` do laço de reposicionamento, // que é stateful. function contarSignificativos(texto, manter) { var n = 0
- for (var i = 0
- i < texto.length
- if (manter.test(texto.charAt(i))) n++ } return n } // Reescreve o valor e recoloca o cursor depois da MESMA quantidade de // caracteres significativos (`manter`), para quem edita no meio do campo não // ver o cursor pular para o fim a cada tecla. function aplicarMascara(el, mascara, manter) { var antes = el.value var pos = typeof el.selectionStart === 'number' ? el.selectionStart : antes.length var significativos = contarSignificativos(antes.slice(0, pos), manter) var depois = mascara(antes) if (depois === antes) return el.value = depois var i = 0 var vistos = 0 while (i < depois.length && vistos < significativos) { if (manter.test(depois.charAt(i))) vistos++ i++ } try { el.setSelectionRange(i, i) } catch (e) { /* input sem seleção (ex.: type=email em alguns browsers) */ } } // (11) 98765-4321 — só para o DDI do Brasil
- outros países ficam livres. function mascaraCelularBR(valor) { var d = String(valor).replace(/\D/g, '').slice(0, 11) if (d.length === 0) return '' if (d.length <= 2) return '(' + d if (d.length <= 7) return '(' + d.slice(0, 2) + ') ' + d.slice(2) return '(' + d.slice(0, 2) + ') ' + d.slice(2, 7) + '-' + d.slice(7) } // R$ 5.000,00 — os dígitos digitados são a parte inteira
- a vírgula (opcional) // abre até 2 casas decimais, completadas no blur. function mascaraMoedaBR(valor) { var s = String(valor).replace(/[^\d,]/g, '') if (!s) return '' var partes = s.split(',') var inteiro = partes[0].replace(/^0+(?=\d)/, '') var decimal = partes.length > 1 ? partes[1].replace(/,/g, '').slice(0, 2) : null var inteiroFmt = inteiro.replace(/\B(?=(\d{3})+(?!\d))/g, '.') || '0' return 'R$ ' + inteiroFmt + (decimal === null ? '' : ',' + decimal) } function completarCentavos(valor) { var s = mascaraMoedaBR(valor) if (!s) return '' var partes = s.split(',') if (partes.length === 1) return s + ',00' return partes[0] + ',' + (partes[1] + '00').slice(0, 2) } // DDI é texto livre: "+" seguido de até 4 dígitos. Vazio ou só "+" conta como // +55 (a sede), para a máscara BR continuar valendo por padrão. function normalizarDDI(valor) { return '+' + String(valor).replace(/\D/g, '').slice(0, 4) } function ddiAtual() { if (!ddiInput) return DDI_BRASIL var v = normalizarDDI(ddiInput.value) return v === '+' ? DDI_BRASIL : v } if (celularInput) { celularInput.addEventListener('input', function () { if (ddiAtual() === DDI_BRASIL) aplicarMascara(celularInput, mascaraCelularBR, /\d/) }) } if (ddiInput && celularInput) { // Ao mudar o código do país, a máscara BR entra (só no +55) ou sai, mantendo // o que já estava digitado no celular. function aoMudarDDI() { if (ddiAtual() === DDI_BRASIL) { celularInput.value = mascaraCelularBR(celularInput.value) celularInput.setAttribute('placeholder', '(00) 00000-0000') } else { celularInput.setAttribute('placeholder', '') } clearError('celular') } ddiInput.addEventListener('input', function () { ddiInput.value = normalizarDDI(ddiInput.value) aoMudarDDI() }) ddiInput.addEventListener('blur', function () { // Sem dígito nenhum, volta para a sede (+55) em vez de deixar só "+". if (!/\d/.test(ddiInput.value)) ddiInput.value = DDI_BRASIL aoMudarDDI() }) } if (moedaInput) { moedaInput.addEventListener('input', function () { aplicarMascara(moedaInput, mascaraMoedaBR, /[\d,]/) }) moedaInput.addEventListener('blur', function () { moedaInput.value = completarCentavos(moedaInput.value) }) } // ── País → Estado / Cidade ───────────────────────────────── // Brasil: estado = lista de UFs e cidade = lista de municípios da UF. // Outro país: os dois viram texto livre (não temos lista para eles). var cacheCidades = {} function paisEhBrasil() { // Sem país escolhido ainda, mantém o padrão brasileiro (caso dominante). var v = paisSelect ? String(paisSelect.value || '') : '' return v === '' || v === PAIS_BRASIL } function preencherCidades(cidades) { if (!cidadesLista) return cidadesLista.textContent = '' if (!cidades || cidades.length === 0) { if (cidadeInput) cidadeInput.removeAttribute('list') return } var frag = document.createDocumentFragment()
- for (var i = 0
- i < cidades.length
var opt = document.createElement('option') opt.value = cidades[i] frag.appendChild(opt) } cidadesLista.appendChild(frag) if (cidadeInput) cidadeInput.setAttribute('list', cidadesLista.id) } function carregarCidades(uf) { if (!uf) { preencherCidades(null) return } if (cacheCidades[uf]) { preencherCidades(cacheCidades[uf]) return } fetch(municipiosUrl + '?uf=' + encodeURIComponent(uf)) .then(function (res) { return res.ok ? res.json() : null }) .then(function (payload) { var cidades = payload && Array.isArray(payload.cidades) ? payload.cidades : null if (!cidades) return cacheCidades[uf] = cidades // Só aplica se a UF ainda for a mesma (respostas podem chegar fora de ordem). if (estadoUf && estadoUf.value === uf && paisEhBrasil()) preencherCidades(cidades) }) .catch(function () { // Sem a lista, o campo continua funcionando como texto livre. }) } function syncLocalizacao(limpar) { var brasil = paisEhBrasil() if (!estadoUf || !estadoLivre) return if (limpar) { estadoUf.value = '' estadoLivre.value = '' if (cidadeInput) cidadeInput.value = '' clearError('estado') clearError('cidade') } // O controle inativo fica `disabled` para não entrar no FormData (os dois // usam name="estado"). estadoUf.disabled = !brasil estadoUf.hidden = !brasil estadoUf.classList.toggle('hidden', !brasil) estadoLivre.disabled = brasil estadoLivre.hidden = brasil estadoLivre.classList.toggle('hidden', brasil) if (estadoLabel) estadoLabel.setAttribute('for', brasil ? 'estado' : 'estadoLivre') if (brasil) carregarCidades(estadoUf.value) else preencherCidades(null) } if (paisSelect) { paisSelect.addEventListener('change', function () { // Estado/cidade pertencem ao país anterior — trocar de país zera os dois. syncLocalizacao(true) }) } if (estadoUf) { estadoUf.addEventListener('change', function () { if (cidadeInput) cidadeInput.value = '' carregarCidades(estadoUf.value) }) } // ── Validação inline ─────────────────────────────────────── // Ignora o controle desativado (estado tem select + input com o mesmo name). function getValue(name) { var els = form.querySelectorAll('[name="' + name + '"]')
- for (var i = 0
- i < els.length
- if (!els[i].disabled) return String(els[i].value || '').trim() } return '' } function celularValido(valor) { if (ddiAtual() === DDI_BRASIL) return CELULAR_BR_RE.test(valor) var digitos = valor.replace(/\D/g, '') return digitos.length >= 6 && digitos.length <= 15 } // Aceita com ou sem protocolo.
- Exige o domínio oficial linkedin.com (ou um // subdomínio, como www./br.) E o caminho de um perfil (/in/algo) — o link tem // que chegar até o perfil, não parar no domínio (linkedin.com sozinho não vale). var LINKEDIN_HOST_RE = /^([a-z0-9-]+\.)*linkedin\.com$/i var LINKEDIN_PERFIL_RE = /^\/in\/[^/]+/i function linkedinValido(valor) { try { var u = new URL(/^https?:\/\//i.test(valor) ? valor : 'https://' + valor) return LINKEDIN_HOST_RE.test(u.hostname) && LINKEDIN_PERFIL_RE.test(u.pathname) } catch (e) { return false } } // Completa o protocolo do que o candidato digitou ("linkedin.com/in/x") para o // servidor receber sempre uma URL absoluta. function normalizarLinkedin(valor) { if (!valor || /^https?:\/\//i.test(valor)) return valor return 'https://' + valor } function radioChecked(name) { return !!form.querySelector('[name="' + name + '"]:checked') } function validate() { clearAllErrors() var ok = true var firstInvalid = null function fail(name, message) { setError(name, message) if (!firstInvalid) firstInvalid = fieldEl(name) ok = false } // 1.
- Currículo (obrigatório + extensão + tamanho) if (!fileInput || !fileInput.files || fileInput.files.length === 0) { fail('curriculo', msg('curriculoObrig')) } else { var file = fileInput.files[0] var parts = file.name.split('.') var ext = parts.length > 1 ? parts.pop().toLowerCase() : '' if (ALLOWED_EXT.indexOf(ext) === -1) { fail('curriculo', msg('curriculoForma')) } else if (file.size > MAX_BYTES) { fail('curriculo', msg('curriculoTamanho')) } } // 2.
- Nome if (!getValue('nome')) fail('nome', msg('nome')) // 3.
- E-mail (vazio e inválido compartilham a mensagem) var email = getValue('email') if (!email || !
- EMAIL_RE.test(email)) fail('email', msg('emailInvalido')) // 4.
- Celular / País / Estado / Cidade var celular = getValue('celular') if (!celular || !celularValido(celular)) fail('celular', msg('celular')) if (!getValue('paisOrigem')) fail('paisOrigem', msg('pais')) if (!getValue('estado')) fail('estado', msg('estado')) if (!getValue('cidade')) fail('cidade', msg('cidade')) // 5.
- LinkedIn (obrigatório — o Bitrix exige o campo na candidatura) var linkedin = getValue('linkedin') if (!linkedin) fail('linkedin', msg('linkedinVazio')) else if (!linkedinValido(linkedin)) fail('linkedin', msg('linkedin')) // Enter dentro do campo envia sem passar pelo blur — normaliza aqui também. else if (linkedinInput) linkedinInput.value = normalizarLinkedin(linkedin) // 6.
- Pretensão (obrigatória: o Bitrix exige current_salary + employment_type) if (!radioChecked('regimePretensao')) fail('pretensao', msg('pretensaoRegime')) else if (!getValue('valorPretensao')) fail('pretensao', msg('pretensaoValor')) else if (moedaInput) moedaInput.value = completarCentavos(moedaInput.value) // 7.
- Aceite de compartilhamento de dados (obrigatório — vira `privacy_policy`) var aceite = form.querySelector('#privacidade') if (!aceite || !aceite.checked) fail('privacidade', msg('privacidade')) if (firstInvalid && typeof firstInvalid.scrollIntoView === 'function') { firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' }) } return ok } // Limpa erro do campo ao editar form.addEventListener('input', function (ev) { var t = ev.target if (!t) return var f = t.closest ? t.closest('.field') : null if (f) { var name = f.getAttribute('data-field') if (name) clearError(name) } }) // ── Validação ao sair do campo (blur) ────────────────────── // `focusout` (e não `blur`) porque só ele borbulha até o <form>. // E-mail: erra também ao sair vazio — é o campo por onde a resposta chega. // Celular e LinkedIn: só cobram formato do que foi digitado; se ficou vazio, // a cobrança é no envio, para não encher a tela de erro em quem só passou // pelo formulário com Tab. form.addEventListener('focusout', function (ev) { var alvo = ev.target if (!alvo) return if (alvo === emailInput) { var email = String(emailInput.value || '').trim() if (!email || !
- EMAIL_RE.test(email)) setError('email', msg('emailInvalido')) else clearError('email') return } if (alvo === celularInput) { var celular = String(celularInput.value || '').trim() if (celular && !celularValido(celular)) setError('celular', msg('celular')) else clearError('celular') return } if (alvo === linkedinInput) { var linkedin = String(linkedinInput.value || '').trim() if (!linkedin) return if (linkedinValido(linkedin)) { linkedinInput.value = normalizarLinkedin(linkedin) clearError('linkedin') } else { setError('linkedin', msg('linkedin')) } } }) // ── Estado do botão ──────────────────────────────────────── // O envio só é liberado com o aceite marcado.
- Duas condições concorrem pelo // `disabled` (aceite + envio em andamento), então elas passam por aqui — // senão o fim do "Enviando…" reabilitaria o botão sem o aceite. var enviando = false function syncSubmitState() { if (!submitBtn) return var aceito = !aceiteInput || aceiteInput.checked submitBtn.disabled = enviando || !aceito } function setLoading(loading) { enviando = loading if (!submitBtn) return submitBtn.setAttribute('aria-busy', loading ? 'true' : 'false') if (submitLabel) { submitLabel.textContent = loading ? (msg('enviando') || 'Enviando…') : submitDefaultLabel } syncSubmitState() } if (aceiteInput) { aceiteInput.addEventListener('change', function () { syncSubmitState() if (aceiteInput.checked) clearError('privacidade') }) } // Estado inicial.
- O botão nasce HABILITADO no HTML de propósito: se o script // não rodar, o formulário ainda funciona (o servidor valida o aceite). syncSubmitState() // ── Submit ───────────────────────────────────────────────── form.addEventListener('submit', function (ev) { ev.preventDefault() setStatus('', null) if (!validate()) { setStatus(msg('reviseCampos'), 'error') return } setLoading(true) var data = new FormData(form) fetch(apiUrl, { method: 'POST', body: data, }) .then(function (res) { if (res.status === 200) { // Sucesso: esconde o form e mostra painel form.classList.add('hidden') form.setAttribute('hidden', '') if (successPanel) { successPanel.classList.remove('hidden') successPanel.removeAttribute('hidden') successPanel.scrollIntoView({ behavior: 'smooth', block: 'center' }) } // Conversão para o GTM (job_application_success ou talent_pool_success). try { window.dataLayer = window.dataLayer || [] window.dataLayer.push(JSON.parse(form.getAttribute('data-track'))) } catch (e) {} return } if (res.status === 400) { return res .json() .catch(function () { return { errors: [] } }) .then(function (payload) { clearAllErrors() var errors = payload && Array.isArray(payload.errors) ? payload.errors : [] if (errors.length === 0) { setStatus(msg('dadosInvalidos'), 'error') return } var firstName = null
- for (var i = 0
- i < errors.length
- var e = errors[i] if (e && e.field) { setError(e.field, e.message || msg('campoInvalido')) if (!firstName) firstName = e.field } } setStatus(msg('reviseCampos'), 'error') var ff = firstName ? fieldEl(firstName) : null if (ff && ff.scrollIntoView) { ff.scrollIntoView({ behavior: 'smooth', block: 'center' }) } }) } // 500 ou outros.
- A mensagem na tela é genérica de propósito, mas o corpo // traz `stage`/`status` do envio ao Bitrix — jogar no console poupa abrir // a aba Network quando alguém relata "deu erro ao enviar". return res .json() .catch(function () { return null }) .then(function (payload) { try { console.error('[candidatura] falha no envio', res.status, payload) } catch (e) { /* console indisponível */ } setStatus(msg('erroEnvio'), 'error') }) }) .catch(function () { setStatus(msg('semConexao'), 'error') }) .then(function () { setLoading(false) }) }) // Estado inicial coerente (país padrão = Brasil → UF em lista, sem limpar o // que o browser tenha restaurado no reload). syncLocalizacao(false) })()