1/** 2 * enrollment-form.js 3 * 4 * Modal de inscrição em cursos: abertura, validação client-side, submissão AJAX 5 * e geração de protocolo de matrÃcula em PDF. 6 * 7 * @package WordPressCertificatePlugin 8 */ 9 10/* global wcpEnrollment */ 11 12( function () { 13 'use strict'; 14 15 document.addEventListener( 'DOMContentLoaded', function () { 16 var modal = document.getElementById( 'wcp-enrollment-modal' ); 17 var form = document.getElementById( 'wcp-enrollment-form' ); 18 var closeBtn = document.getElementById( 'wcp-modal-close' ); 19 var titleEl = document.getElementById( 'wcp-modal-title' ); 20 var cursoIdEl = document.getElementById( 'wcp-curso-matricula-id' ); 21 var cursoSrcEl = document.getElementById( 'wcp-curso-source' ); 22 var nonceEl = document.getElementById( 'wcp-enrollment-nonce' ); 23 var submitBtn = document.getElementById( 'wcp-enrollment-submit' ); 24 var successArea = document.getElementById( 'wcp-enrollment-success' ); 25 var successMsg = document.getElementById( 'wcp-success-message' ); 26 var generalErr = document.getElementById( 'wcp-form-error-general' ); 27 28 if ( ! modal || ! form ) { return; } 29 30 var fields = { 31 nome: document.getElementById( 'wcp-enrollment-nome' ), 32 cpf: document.getElementById( 'wcp-enrollment-cpf' ), 33 email: document.getElementById( 'wcp-enrollment-email' ), 34 telefone: document.getElementById( 'wcp-enrollment-telefone' ), 35 numero_cref: document.getElementById( 'wcp-enrollment-cref' ), 36 validade_cip: document.getElementById( 'wcp-enrollment-validade-cip' ) 37 }; 38 39 var errorEls = { 40 nome: document.getElementById( 'wcp-error-nome' ), 41 cpf: document.getElementById( 'wcp-error-cpf' ), 42 email: document.getElementById( 'wcp-error-email' ), 43 telefone: document.getElementById( 'wcp-error-telefone' ), 44 numero_cref: document.getElementById( 'wcp-error-cref' ), 45 validade_cip: document.getElementById( 'wcp-error-validade-cip' ) 46 }; 47 48 /* ââ Máscara de CPF âââââââââââââââââââââââââââââââââââââââââââââââââ */ 49 50 if ( fields.cpf ) { 51 fields.cpf.addEventListener( 'input', function () { 52 var v = this.value.replace( /\D/g, '' ).substring( 0, 11 ); 53 if ( v.length > 9 ) { 54 this.value = v.replace( /(\d{3})(\d{3})(\d{3})(\d{1,2})/, '$1.$2.$3-$4' ); 55 } else if ( v.length > 6 ) { 56 this.value = v.replace( /(\d{3})(\d{3})(\d{1,3})/, '$1.$2.$3' ); 57 } else if ( v.length > 3 ) { 58 this.value = v.replace( /(\d{3})(\d{1,3})/, '$1.$2' ); 59 } else { 60 this.value = v; 61 } 62 } ); 63 } 64 65 /* ââ Máscara de WhatsApp / telefone âââââââââââââââââââââââââââââââââ */ 66 67 if ( fields.telefone ) { 68 fields.telefone.addEventListener( 'input', function () { 69 var v = this.value.replace( /\D/g, '' ).substring( 0, 11 ); 70 if ( v.length > 10 ) { 71 // Celular com 11 dÃgitos: (XX) XXXXX-XXXX 72 this.value = v.replace( /(\d{2})(\d{5})(\d{1,4})/, '($1) $2-$3' ); 73 } else if ( v.length > 6 ) { 74 // Fixo com 10 dÃgitos: (XX) XXXX-XXXX 75 this.value = v.replace( /(\d{2})(\d{4})(\d{1,4})/, '($1) $2-$3' ); 76 } else if ( v.length > 2 ) { 77 this.value = v.replace( /(\d{2})(\d{1,5})/, '($1) $2' ); 78 } else if ( v.length > 0 ) { 79 this.value = v.replace( /(\d{1,2})/, '($1' ); 80 } else { 81 this.value = v; 82 } 83 } ); 84 } 85 86 /* ââ Abrir Modal ââââââââââââââââââââââââââââââââââââââââââââââââ */ 87 88 var tsEl = document.getElementById( 'wcp-form-ts' ); 89 90 function openModal( cursoNome, cursoId, cursoSource ) { 91 titleEl.textContent = cursoNome; 92 cursoIdEl.value = cursoId; 93 if ( cursoSrcEl ) { cursoSrcEl.value = cursoSource || 'legacy'; } 94 nonceEl.value = wcpEnrollment.nonce; 95 96 form.removeAttribute( 'hidden' ); 97 successArea.setAttribute( 'hidden', '' ); 98 generalErr.setAttribute( 'hidden', '' ); 99 generalErr.textContent = ''; 100 clearAllErrors(); 101 form.reset(); 102 103 // Marca o instante de abertura para o time-trap anti-bot.
104 if ( tsEl ) { tsEl.value = String( Date.now() ); } 105 106 modal.removeAttribute( 'hidden' ); 107 modal.setAttribute( 'aria-hidden', 'false' ); 108 109 if ( fields.nome ) { fields.nome.focus(); } 110 } 111 112 function closeModal() { 113 modal.setAttribute( 'hidden', '' ); 114 modal.setAttribute( 'aria-hidden', 'true' ); 115 } 116 117 /* ââ Delegação de clique nos botões "Inscrever-se" ââââââââââââââ */ 118 119 document.addEventListener( 'click', function ( e ) { 120 var btn = e.target.closest( '.wcp-btn-inscrever' ); 121 if ( ! btn ) { return; } 122 123 var nome = btn.getAttribute( 'data-curso-nome' ); 124 var id = btn.getAttribute( 'data-curso-id' ); 125 var source = btn.getAttribute( 'data-curso-source' ) || 'legacy'; 126 127 if ( nome && id ) { 128 openModal( nome, id, source ); 129 } 130 } ); 131 132 /* ââ Fechar modal âââââââââââââââââââââââââââââââââââââââââââââââ */ 133 134 if ( closeBtn ) { 135 closeBtn.addEventListener( 'click', closeModal ); 136 } 137 138 modal.addEventListener( 'click', function ( e ) { 139 if ( e.target === modal ) { closeModal(); } 140 } ); 141 142 document.addEventListener( 'keydown', function ( e ) { 143 if ( e.key === 'Escape' && ! modal.hasAttribute( 'hidden' ) ) { 144 closeModal(); 145 } 146 } ); 147 148 /* ââ Limpar erros ao digitar ââââââââââââââââââââââââââââââââââââ */ 149 150 Object.keys( fields ).forEach( function ( key ) { 151 if ( fields[ key ] ) { 152 fields[ key ].addEventListener( 'input', function () { 153 clearFieldError( key ); 154 } ); 155 } 156 } ); 157 158 /* ââ Validação ââââââââââââââââââââââââââââââââââââââââââââââââââ */ 159 160 function validateFields() { 161 var errors = {}; 162 var nome = ( fields.nome ? fields.nome.value.trim() : '' ); 163 if ( ! nome ) { errors.nome = 'O nome é obrigatório.'; } 164 165 // CPF validation 166 var cpfRaw = ( fields.cpf ? fields.cpf.value.replace( /\D/g, '' ) : '' ); 167 if ( ! cpfRaw ) { errors.cpf = 'O CPF é obrigatório.'; } 168 else if ( cpfRaw.length !== 11 ) { errors.cpf = 'CPF deve ter 11 dÃgitos.'; } 169 else if ( ! validateCpfDigits( cpfRaw ) ) { errors.cpf = 'CPF inválido.'; } 170 171 // E-mail é opcional; valida formato apenas se preenchido. 172 var email = ( fields.email ? fields.email.value.trim() : '' ); 173 if ( email && ! /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( email ) ) { errors.email = 'Formato de e-mail inválido.'; } 174 175 var telefone = ( fields.telefone ? fields.telefone.value.replace( /\D/g, '' ) : '' ); 176 if ( ! telefone ) { errors.telefone = 'O WhatsApp é obrigatório.'; } 177 else if ( ! /^\d{10,11}$/.test( telefone ) ) { errors.telefone = 'O WhatsApp deve ter 10 ou 11 dÃgitos (com DDD).'; } 178 179 var cref = ( fields.numero_cref ? fields.numero_cref.value.trim() : '' ); 180 if ( ! cref ) { errors.numero_cref = 'O número CREF é obrigatório.'; } 181 else if ( ! /^[a-zA-Z0-9]{4,20}$/.test( cref.replace( /[^a-zA-Z0-9]/g, '' ) ) ) { errors.numero_cref = 'Entre 4 e 20 caracteres alfanuméricos.'; } 182 183 return errors; 184 } 185 186 function validateCpfDigits( cpf ) { 187 if ( /^(\d)\1{10}$/.test( cpf ) ) return false; 188 var sum = 0, i; 189 for ( i = 0; i < 9; i++ ) sum += parseInt( cpf[i] ) * ( 10 - i ); 190 var rem = sum % 11; 191 var d1 = rem < 2 ? 0 : 11 - rem; 192 if ( parseInt( cpf[9] ) !== d1 ) return false; 193 sum = 0; 194 for ( i = 0; i < 10; i++ ) sum += parseInt( cpf[i] ) * ( 11 - i ); 195 rem = sum % 11; 196 var d2 = rem < 2 ? 0 : 11 - rem; 197 return parseInt( cpf[10] ) === d2; 198 } 199 200 /* ââ Utilitários de erro ââââââââââââââââââââââââââââââââââââââââ */ 201 202 function showFieldError( field, message ) { 203 if ( errorEls[ field ] ) { errorEls[ field ].textContent = message; } 204 if ( fields[ field ] ) { fields[ field ].classList.add( 'wcp-input-error' ); } 205 } 206 207 function clearFieldError( field ) { 208 if ( errorEls[ field ] ) { errorEls[ field ].textContent = ''; } 209 if ( fields[ field ] ) { fields[ field ].classList.remove( 'wcp-input-error' ); } 210 } 211 212 function clearAllErrors() { 213 Object.keys( errorEls ).forEach( function ( key ) { clearFieldError( key ); } ); 214 } 215 216 function showFieldErrors( errors ) { 217 Object.keys( errors ).forEach( function ( key ) { showFieldError( key, errors[ key ] ); } ); 218 } 219 220 function showGeneralError( message ) { 221 if ( generalErr ) { 222 generalErr.textContent = message; 223 generalErr.removeAttribute( 'hidden' ); 224 } 225 } 226 227 /* ââ Geração de Protocolo PDF (padrão governamental) ââââââââââââ */ 228 229 function generateProtocol( data ) { 230 var now = new Date(); 231 var protocolo = data.protocolo || ( 'MAT' + now.getFullYear() + pad( now.getMonth() + 1 ) + pad( now.getDate() ) + pad( now.getHours() ) + pad( now.getMinutes() ) + pad( now.getSeconds() ) + String( data.enrollment_id ).padStart( 5, '0' ) ); 232 var dataHora = pad( now.getDate() ) + '/' + pad( now.getMonth() + 1 ) + '/' + now.getFullYear() + ' à s ' + pad( now.getHours() ) + ':' + pad( now.getMinutes() ) + ':' + pad( now.getSeconds() ); 233 var cursoNome = titleEl.textContent; 234 var nome = fields.nome ? fields.nome.value.trim() : ''; 235 var email = fields.email ? fields.email.value.trim() : ''; 236 var telefone = fields.telefone ? fields.telefone.value : ''; 237 var cref = fields.numero_cref ? fields.numero_cref.value.trim() : '';
238 var validadeCip = fields.validade_cip ? fields.validade_cip.value : ''; 239 240 // Código de autenticação (hash simples para verificação) 241 var authCode = protocolo.split( '' ).reverse().join( '' ).substring( 0, 8 ).toUpperCase(); 242 243 var html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Protocolo de MatrÃcula</title>' 244 + '<style>' 245 + 'body{font-family:"Segoe UI",Roboto,Arial,sans-serif;margin:0;padding:40px;color:#1a1a1a;font-size:14px;}' 246 + '.protocolo{max-width:700px;margin:0 auto;border:2px solid #35515b;border-radius:4px;padding:0;}' 247 + '.header{background:#35515b;color:#fff;padding:24px 32px;display:flex;align-items:center;justify-content:space-between;}' 248 + '.header h1{margin:0;font-size:18px;font-weight:700;letter-spacing:0.5px;}' 249 + '.header .subtitulo{margin:4px 0 0;font-size:12px;opacity:0.85;}' 250 + '.brasao{font-size:28px;}' 251 + '.corpo{padding:32px;}' 252 + '.numero-protocolo{background:#f0f4f5;border:1px solid #d4dee1;border-radius:6px;padding:16px 24px;margin-bottom:24px;text-align:center;}' 253 + '.numero-protocolo .label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#64748b;margin-bottom:4px;}' 254 + '.numero-protocolo .valor{font-size:22px;font-weight:700;color:#35515b;font-family:monospace;letter-spacing:2px;}' 255 + '.secao{margin-bottom:20px;}' 256 + '.secao-titulo{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#64748b;border-bottom:1px solid #e2e8f0;padding-bottom:6px;margin-bottom:12px;font-weight:600;}' 257 + '.campo{display:flex;margin-bottom:8px;}' 258 + '.campo .rotulo{font-weight:600;color:#374151;min-width:160px;}' 259 + '.campo .valor{color:#1e293b;}' 260 + '.rodape{border-top:1px solid #e2e8f0;padding:20px 32px;display:flex;justify-content:space-between;align-items:center;font-size:11px;color:#64748b;}' 261 + '.autenticacao{font-family:monospace;font-size:13px;color:#35515b;font-weight:600;letter-spacing:1px;}' 262 + '.aviso{background:#fefce8;border:1px solid #fde047;border-radius:6px;padding:12px 16px;margin-top:20px;font-size:12px;color:#854d0e;}' 263 + '@media print{body{padding:20px;}.protocolo{border-width:1px;}}' 264 + '</style></head><body>' 265 + '<div class="protocolo">' 266 + '<div class="header"><div><h1>COMPROVANTE DE MATRÃCULA</h1><p class="subtitulo">Protocolo de Inscrição em Curso</p></div><div class="brasao">ð</div></div>' 267 + '<div class="corpo">' 268 + '<div class="numero-protocolo"><div class="label">Número do Protocolo</div><div class="valor">' + protocolo + '</div></div>' 269 + '<div class="secao"><div class="secao-titulo">Dados do Solicitante</div>' 270 + '<div class="campo"><span class="rotulo">Nome completo:</span><span class="valor">' + escHtml( nome ) + '</span></div>' 271 + ( email ? '<div class="campo"><span class="rotulo">E-mail:</span><span class="valor">' + escHtml( email ) + '</span></div>' : '' ) 272 + '<div class="campo"><span class="rotulo">WhatsApp:</span><span class="valor">' + formatPhone( telefone ) + '</span></div>' 273 + '<div class="campo"><span class="rotulo">Nº CREF:</span><span class="valor">' + escHtml( cref ) + '</span></div>' 274 + ( validadeCip ? '<div class="campo"><span class="rotulo">Validade da CIP:</span><span class="valor">' + escHtml( formatDateBr( validadeCip ) ) + '</span></div>' : '' ) 275 + '</div>' 276 + '<div class="secao"><div class="secao-titulo">Dados do Serviço</div>' 277 + '<div class="campo"><span class="rotulo">Serviço:</span><span class="valor">MatrÃcula em Curso</span></div>' 278 + '<div class="campo"><span class="rotulo">Curso:</span><span class="valor">' + escHtml( cursoNome ) + '</span></div>' 279 + '<div class="campo"><span class="rotulo">Data/Hora da Solicitação:</span><span class="valor">' + dataHora + '</span></div>' 280 + '<div class="campo"><span class="rotulo">Situação:</span><span class="valor" style="color:#059669;font-weight:600;">â MatrÃcula Confirmada</span></div>' 281 + '</div>' 282 + '<div class="aviso">â ï¸ Este documento é o comprovante oficial da sua matrÃcula. Guarde-o para apresentação no dia do curs
282o. A autenticidade pode ser verificada pelo código abaixo.</div>' 283 + '</div>' 284 + '<div class="rodape"><span>Emitido em: ' + dataHora + '</span><span class="autenticacao">Cód. Autenticação: ' + authCode + '</span></div>' 285 + '</div></body></html>'; 286 287 // Abrir em nova janela para impressão/download 288 var win = window.open( '', '_blank' ); 289 if ( win ) { 290 win.document.write( html ); 291 win.document.close(); 292 setTimeout( function () { win.print(); }, 500 ); 293 } 294 } 295 296 function pad( n ) { return n < 10 ? '0' + n : '' + n; } 297 function escHtml( s ) { var d = document.createElement( 'div' ); d.textContent = s; return d.innerHTML; } 298 function formatDateBr( ymd ) { 299 var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec( ymd || '' ); 300 return m ? m[3] + '/' + m[2] + '/' + m[1] : ymd; 301 } 302 function formatPhone( p ) { 303 var d = p.replace( /\D/g, '' ); 304 if ( d.length === 11 ) return '(' + d.substr(0,2) + ') ' + d.substr(2,5) + '-' + d.substr(7); 305 if ( d.length === 10 ) return '(' + d.substr(0,2) + ') ' + d.substr(2,4) + '-' + d.substr(6); 306 return p; 307 } 308 309 /* ââ Submissão AJAX âââââââââââââââââââââââââââââââââââââââââââââ */ 310 311 form.addEventListener( 'submit', function ( e ) { 312 e.preventDefault(); 313 314 clearAllErrors(); 315 generalErr.setAttribute( 'hidden', '' ); 316 generalErr.textContent = ''; 317 318 var errors = validateFields(); 319 if ( Object.keys( errors ).length > 0 ) { 320 showFieldErrors( errors ); 321 return; 322 } 323 324 var body = new FormData(); 325 body.append( 'action', 'wcp_enrollment' ); 326 body.append( '_wpnonce', nonceEl.value ); 327 body.append( 'curso_matricula_id', cursoIdEl.value ); 328 body.append( 'curso_source', cursoSrcEl ? cursoSrcEl.value : 'legacy' ); 329 body.append( 'nome', fields.nome.value.trim() ); 330 body.append( 'cpf', fields.cpf ? fields.cpf.value : '' ); 331 body.append( 'email', fields.email ? fields.email.value.trim() : '' ); 332 body.append( 'telefone', fields.telefone.value.replace( /\D/g, '' ) ); 333 body.append( 'numero_cref', fields.numero_cref ? fields.numero_cref.value.trim() : '' ); 334 body.append( 'validade_cip', fields.validade_cip ? fields.validade_cip.value : '' ); 335 336 // Campos anti-bot: honeypot (deve ficar vazio) e timestamp de abertura. 337 var hpEl = document.getElementById( 'wcp-website' ); 338 body.append( 'website', hpEl ? hpEl.value : '' ); 339 body.append( 'wcp_form_ts', tsEl ? tsEl.value : '' ); 340 341 submitBtn.disabled = true; 342 submitBtn.textContent = 'Processandoâ¦'; 343 344 fetch( wcpEnrollment.ajaxurl, { 345 method: 'POST', 346 body: body, 347 credentials: 'same-origin' 348 } ) 349 .then( function ( response ) { 350 return response.json().then( function ( data ) { 351 return { status: response.status, data: data }; 352 } ); 353 } ) 354 .then( function ( result ) { 355 submitBtn.disabled = false; 356 submitBtn.textContent = 'Inscrever-se'; 357 358 if ( result.data.success ) { 359 var respData = result.data.data || {}; 360 form.setAttribute( 'hidden', '' ); 361 362 // Exibir tela de sucesso com opção de baixar protocolo 363 successArea.innerHTML = '<div class="wcp-success-content">' 364 + '<div class="wcp-success-icon">â</div>' 365 + '<h3 class="wcp-success-title">MatrÃcula Confirmada!</h3>' 366 + '<p class="wcp-success-desc">Sua inscrição foi registrada com sucesso.</p>' 367 + '<button type="button" class="wcp-btn-protocol" id="wcp-download-protocol">ð Baixar Comprovante de MatrÃcula</button>' 368 + '<p class="wcp-success-hint">
368Guarde o comprovante para apresentação no dia do curso.</p>' 369 + '</div>'; 370 successArea.removeAttribute( 'hidden' ); 371 372 // Evento do botão de download 373 var dlBtn = document.getElementById( 'wcp-download-protocol' ); 374 if ( dlBtn ) { 375 dlBtn.addEventListener( 'click', function () { 376 generateProtocol( respData ); 377 } ); 378 } 379 } else if ( result.status === 422 && result.data.data && result.data.data.errors ) { 380 showFieldErrors( result.data.data.errors ); 381 if ( result.data.data.message ) { showGeneralError( result.data.data.message ); } 382 } else { 383 var errorMsg = result.data.data && result.data.data.message 384 ? result.data.data.message 385 : 'Ocorreu um erro. Tente novamente mais tarde.'; 386 showGeneralError( errorMsg ); 387 } 388 } ) 389 .catch( function () { 390 submitBtn.disabled = false; 391 submitBtn.textContent = 'Inscrever-se'; 392 showGeneralError( 'Erro de comunicação. Verifique sua conexão e tente novamente.' ); 393 } ); 394 } ); 395 } ); 396}() );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.