1document.addEventListener('DOMContentLoaded', () => { 2 const form = document.getElementById('form-alta-final'); 3 if (!form) return; 4 5 if (form.dataset.inicializado === '1') return; 6 form.dataset.inicializado = '1'; 7 8 9 const STORAGE_KEY = 'mi_form_alta_borrador'; 10 11 const getDatosFormulario = () => ({ 12 nombre: form.querySelector('#nombre')?.value || '', 13 apellidos: form.querySelector('#apellidos')?.value || '', 14 fecha: form.querySelector('#fecha')?.value || '', 15 correo: form.querySelector('#correo')?.value || '', 16 telefono: form.querySelector('#telefono')?.value || '', 17 provincia: form.querySelector('#provincia')?.value || '', 18 localidad: form.querySelector('#localidad')?.value || '', 19 postal: form.querySelector('#postal')?.value || '', 20 direcc: form.querySelector('#direcc')?.value || '', 21 paquete: form.querySelector('#paquete')?.value || '', 22 acepta_privacidad: !!form.querySelector('#acepta_privacidad')?.checked 23 }); 24 25 const guardarBorrador = () => { 26 try { 27 const datos = getDatosFormulario(); 28 sessionStorage.setItem(STORAGE_KEY, JSON.stringify(datos)); 29 } catch (e) {} 30 }; 31 32 const restaurarBorrador = async () => { 33 try { 34 const raw = sessionStorage.getItem(STORAGE_KEY); 35 if (!raw) return; 36 37 const datos = JSON.parse(raw); 38 39 if (form.querySelector('#nombre')) form.querySelector('#nombre').value = datos.nombre || ''; 40 if (form.querySelector('#apellidos')) form.querySelector('#apellidos').value = datos.apellidos || ''; 41 if (form.querySelector('#fecha')) form.querySelector('#fecha').value = datos.fecha || ''; 42 if (form.querySelector('#correo')) form.querySelector('#correo').value = datos.correo || ''; 43 if (form.querySelector('#telefono')) form.querySelector('#telefono').value = datos.telefono || ''; 44 if (form.querySelector('#postal')) form.querySelector('#postal').value = datos.postal || ''; 45 if (form.querySelector('#direcc')) form.querySelector('#direcc').value = datos.direcc || ''; 46 if (form.querySelector('#paquete')) form.querySelector('#paquete').value = datos.paquete || ''; 47 48 if (form.querySelector('#acepta_privacidad')) { 49 form.querySelector('#acepta_privacidad').checked = !!datos.acepta_privacidad; 50 } 51 52 if (provincia && datos.provincia) { 53 provincia.value = datos.provincia; 54 await cargarLocalidades(datos.provincia, {
55 localidadSeleccionada: datos.localidad || '', 56 silencioso: true 57 }); 58 } 59 60 // Refrescar estados visuales sin disparar todos los listeners 61 form.querySelectorAll('.form-grupo.flotante input, .form-grupo.flotante select').forEach(el => { 62 actualizarEstadoFlotante(el); 63 }); 64 65 fields.forEach(f => { 66 validateFieldById(f.id); 67 }); 68 69 actualizarProgreso(); 70 } catch (e) {} 71 }; 72 73 const limpiarBorrador = () => { 74 try { 75 sessionStorage.removeItem(STORAGE_KEY); 76 } catch (e) {} 77 }; 78 79 form.addEventListener('input', (e) => { 80 const el = e.target; 81 if (!el || el.type === 'file') return; 82 guardarBorrador(); 83 }); 84 85 form.addEventListener('change', (e) => { 86 const el = e.target; 87 if (!el || el.type === 'file') return; 88 guardarBorrador(); 89 }); 90 91 92 93 94 95 const provincia = form.querySelector('#provincia'); 96 const contLocalidad = form.querySelector('#contenedor-localidad'); 97 const localidad = form.querySelector('#localidad'); 98 const btnEnviar = form.querySelector('#btn-enviar'); 99 const progresoFill = form.querySelector('#form-progreso-fill'); 100 const progresoPorcentaje = form.querySelector('#progreso-porcentaje'); 101 const resumenErrores = form.querySelector('#resumen-errores-formulario'); 102 103 104 function activarEstadoEnvio() { 105 form.dataset.enviando = '1'; 106 form.classList.add('enviando'); 107 108 if (btnEnviar) { 109 btnEnviar.disabled = true; 110 btnEnviar.innerText = 'ENVIANDO...'; 111 btnEnviar.classList.add('is-loading'); 112 } 113 114 let overlay = form.querySelector('.overlay-envio'); 115 if (!overlay) { 116 overlay = document.createElement('div'); 117 overlay.className = 'overlay-envio'; 118 overlay.innerHTML = ` 119 <div class="overlay-envio-contenido"> 120 <div class="spinner-envio" aria-hidden="true"></div> 121 <div class="overlay-envio-texto">Estamos enviando tu solicitud. No cierres esta página.</div> 122 </div> 123 `; 124 form.appendChild(overlay); 125 } 126 } 127 128 function desactivarEstadoEnvio() { 129 form.dataset.enviando = '0'; 130 form.classList.remove('enviando'); 131 132 if (btnEnviar) { 133 btnEnviar.disabled = false; 134 btnEnviar.innerText = 'ENVIAR SOLICITUD DE ALTA'; 135 btnEnviar.classList.remove('is-loading'); 136 } 137 138 const overlay = form.querySelector('.overlay-envio'); 139 if (overlay) { 140 overlay.remove(); 141 } 142 } 143 144 const getErrorSpan = (el) => el?.parentElement?.querySelector?.('.error-mensaje') || null; 145 146 const setError = (el, msg) => { 147 const span = getErrorSpan(el); 148 if (span) span.textContent = msg || ''; 149 150 if (el && typeof el.setCustomValidity === 'function') { 151 el.setCustomValidity(msg ? msg : ''); 152 } 153 154 const grupo = el?.closest('.form-grupo'); 155 if (grupo) { 156 if (msg) grupo.classList.add('tiene-error'); 157 else grupo.classList.remove('tiene-error'); 158 } 159 160 if (msg) el?.setAttribute('aria-invalid', 'true'); 161 else el?.removeAttribute('aria-invalid'); 162 }; 163 164 const normalizeSpaces = (s) => (s || '').replace(/\s+/g, ' ').trim(); 165 166 // Relación entre provincia y prefijo real de código postal en España. 167 const provincePostalPrefixes = {
168 'ÃLAVA':'01','ALAVA':'01','ALBACETE':'02','ALICANTE':'03','ALACANT':'03','ALMERÃA':'04','ALMERIA':'04','ÃVILA':'05','AVILA':'05','BADAJOZ':'06', 169 'ILLES BALEARS':'07','ISLAS BALEARES':'07','BALEARES':'07','BARCELONA':'08','BURGOS':'09','CÃCERES':'10','CACERES':'10','CÃDIZ':'11','CADIZ':'11', 170 'CASTELLÃN':'12','CASTELLON':'12','CASTELLÃ':'12','CIUDAD REAL':'13','CÃRDOBA':'14','CORDOBA':'14','A CORUÃA':'15','LA CORUÃA':'15','CORUÃA':'15', 171 'CUENCA':'16','GIRONA':'17','GERONA':'17','GRANADA':'18','GUADALAJARA':'19','GIPUZKOA':'20','GUIPÃZCOA':'20','GUIPUZCOA':'20','HUELVA':'21','HUESCA':'22', 172 'JAÃN':'23','JAEN':'23','LEÃN':'24','LEON':'24','LLEIDA':'25','LÃRIDA':'25','LERIDA':'25','LA RIOJA':'26','RIOJA':'26','LUGO':'27','MADRID':'28', 173 'MÃLAGA':'29','MALAGA':'29','MURCIA':'30','NAVARRA':'31','OURENSE':'32','ORENSE':'32','ASTURIAS':'33','PALENCIA':'34','LAS PALMAS':'35','PONTEVEDRA':'36', 174 'SALAMANCA':'37','SANTA CRUZ DE TENERIFE':'38','CANTABRIA':'39','SEGOVIA':'40','SEVILLA':'41','SORIA':'42','TARRAGONA':'43','TERUEL':'44','TOLEDO':'45', 175 'VALENCIA':'46','VALÃNCIA':'46','VALLADOLID':'47','BIZKAIA':'48','VIZCAYA':'48','ZAMORA':'49','ZARAGOZA':'50','CEUTA':'51','MELILLA':'52' 176 }; 177 178 const normalizeProvinceName = (text) => (text || '').trim().toUpperCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); 179 180 const getSelectedProvincePrefix = () => { 181 if (!provincia) return ''; 182 const option = provincia.options[provincia.selectedIndex]; 183 if (!option) return ''; 184 185 const rawName = (option.textContent || '').trim().toUpperCase(); 186 return provincePostalPrefixes[rawName] || provincePostalPrefixes[normalizeProvinceName(rawName)] || ''; 187 }; 188 189 const calculateSpanishDocumentLetter = (number) => { 190 const letters = 'TRWAGMYFPDXBNJZSQVHLCKE'; 191 return letters[Number(number) % 23] || ''; 192 }; 193 194 const isValidDniNie = (value) => { 195 const v = (value || '').toUpperCase().replace(/\s+/g, ''); 196 197 if (/^\d{8}[A-Z]$/.test(v)) { 198 return calculateSpanishDocumentLetter(v.slice(0, 8)) === v.slice(-1); 199 } 200 201 if (/^[XYZ]\d{7}[A-Z]$/.test(v)) { 202 const map = { X: '0', Y: '1', Z: '2' }; 203 const number = map[v[0]] + v.slice(1, 8); 204 return calculateSpanishDocumentLetter(number) === v.slice(-1); 205 } 206 207 return false; 208 }; 209 210 const validators = { 211 nombre: (v) => { 212 v = normalizeSpaces(v); 213 if (!v) return 'El nombre es obligatorio.'; 214 if (v.length < 2) return 'El nombre es demasiado corto.'; 215 if (v.length > 60) return 'El nombre es demasiado largo.'; 216 if (!/^[A-Za-zÃÃÃÃÃÃÃáéÃóúüñ\s'-]+$/.test(v)) return 'El nombre solo puede contener letras y espacios.'; 217 return ''; 218 }, 219 apellidos: (v) => { 220 v = normalizeSpaces(v); 221 if (!v) return 'Los apellidos son obligatorios.'; 222 if (v.length < 2) return 'Los apellidos son demasiado cortos.'; 223 if (v.length > 100) return 'Los apellidos son demasiado largos.'; 224 if (!/^[A-Za-zÃÃÃÃÃÃÃáéÃóúüñ\s'-]+$/.test(v)) return 'Los apellidos solo pueden contener letras y espacios.'; 225 return ''; 226 }, 227 dni: (v) => { 228 v = (v || '').toUpperCase().replace(/\s+/g, ''); 229 if (!v) return 'El DNI/NIE es obligatorio.'; 230 if (!/^\d{8}[A-Z]$/.test(v) && !/^[XYZ]\d{7}[A-Z]$/.test(v)) { 231 return 'Formato de DNI/NIE no válido. Ej: 12345678Z o X1234567L.'; 232 } 233 if (!isValidDniNie(v)) return 'La letra del DNI/NIE no es correcta.'; 234 return ''; 235 }, 236 fecha: (v) => { 237 if (!v) return 'La fecha de nacimiento es obligatoria.'; 238 239 const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v); 240 if (!m) return 'Fecha no válida.'; 241 242 const yyyy = parseInt(m[1], 10); 243 const mm = parseInt(m[2], 10); 244 const dd = parseInt(m[3], 10); 245 246 const d = new Date(yyyy, mm - 1, dd); 247 if ( 248 Number.isNaN(d.getTime()) || 249 d.getFullYear() !== yyyy || 250 d.getMonth() !== mm - 1 || 251 d.getDate() !== dd 252 ) { 253 return 'Fecha no válida.'; 254 } 255 256 const hoy = new Date(); 257 hoy.setHours(0, 0, 0, 0); 258 259 if (d > hoy) return 'La fecha no puede ser futura.'; 260 261 let edad = hoy.getFullYear() - yyyy; 262 const mesActual = hoy.getMonth() + 1; 263 const diaActual = hoy.getDate(); 264 265 if (mesActual < mm || (mesActual === mm && diaActual < dd)) { 266 edad--; 267 } 268 269 if (edad < 18) return 'Debes ser mayor de 18 años.'; 270 271 return ''; 272 }, 273 correo: (v) => { 274 v = (v || '').trim(); 275 if (!v) return 'El correo es obligatorio.'; 276 if (v.length > 120) return 'El correo es demasiado largo.'; 277 if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) return 'Introduce un correo válido (ej: [email protected]).'; 278 return ''; 279 }, 280 telefono: (v) => { 281 v = (v || '').replace(/\s+/g, ''); 282 if (!v) return 'El teléfono es obligatorio.'; 283 if (!/^\d{9}$/.test(v)) return 'El teléfono debe tener 9 dÃgitos.'; 284 if (!/^[6789]\d{8}$/.test(v)) return 'El teléfono debe empezar por 6, 7, 8 o 9.'; 285 return ''; 286 }, 287 provincia: (v) => (!v ? 'Selecciona una provincia.' : ''), 288 localidad: (v) => { 289 if (localidad && localidad.disabled) return ''; 290 return (!v ? 'Selecciona una localidad.' : ''); 291 }, 292 postal: (v) => { 293 v = (v || '').trim(); 294 if (!v) return 'El código postal es obligatorio.'; 295 if (!/^\d{5}$/.test(v)) return 'El código postal debe tener 5 dÃgitos.'; 296 const prov = parseInt(v.slice(0, 2), 10); 297 if (prov < 1 || prov > 52) return 'Código postal no válido en España.'; 298 const expectedPrefix = getSelectedProvincePrefix(); 299 if (expectedPrefix && v.slice(0, 2) !== expectedPrefix) { 300 return 'El código postal no coincide con la provincia seleccionad
300a.'; 301 } 302 return ''; 303 }, 304 direcc: (v) => { 305 v = normalizeSpaces(v); 306 if (!v) return 'La dirección es obligatoria.'; 307 if (v.length < 6) return 'La dirección es demasiado corta.'; 308 if (v.length > 150) return 'La dirección es demasiado larga.'; 309 return ''; 310 }, 311 paquete: (v) => (!v ? 'Selecciona una tarifa.' : ''), 312 foto_dni_delante: () => { 313 const f = form.querySelector('#foto_dni_delante')?.files?.[0]; 314 if (!f) return 'Sube la foto del DNI (anverso).'; 315 if (!['image/jpeg', 'image/png', 'image/webp'].includes(f.type)) return 'La imagen debe ser JPG, PNG o WEBP.'; 316 if (f.size > 5 * 1024 * 1024) return 'La imagen supera 5MB.'; 317 return ''; 318 }, 319 foto_dni_detras: () => { 320 const f = form.querySelector('#foto_dni_detras')?.files?.[0]; 321 if (!f) return 'Sube la foto del DNI (reverso).'; 322 if (!['image/jpeg', 'image/png', 'image/webp'].includes(f.type)) return 'La imagen debe ser JPG, PNG o WEBP.'; 323 if (f.size > 5 * 1024 * 1024) return 'La imagen supera 5MB.'; 324 return ''; 325 }, 326 327 acepta_privacidad: () => { 328 const checked = !!form.querySelector('#acepta_privacidad')?.checked; 329 return checked ? '' : 'Debes aceptar la polÃtica de privacidad.'; 330 } 331 }; 332 333 334 335 const fields = [ 336 { id: 'nombre', key: 'nombre' }, 337 { id: 'apellidos', key: 'apellidos' }, 338 { id: 'dni', key: 'dni' }, 339 { id: 'fecha', key: 'fecha' }, 340 { id: 'correo', key: 'correo' }, 341 { id: 'telefono', key: 'telefono' }, 342 { id: 'provincia', key: 'provincia' }, 343 { id: 'localidad', key: 'localidad' }, 344 { id: 'postal', key: 'postal' }, 345 { id: 'direcc', key: 'direcc' }, 346 { id: 'paquete', key: 'paquete' }, 347 { id: 'foto_dni_delante', key: 'foto_dni_delante' }, 348 { id: 'foto_dni_detras', key: 'foto_dni_detras' }, 349 { id: 'acepta_privacidad', key: 'acepta_privacidad' } 350 ]; 351 352 const validateFieldById = (id) => { 353 const el = form.querySelector('#' + id); 354 const def = fields.find(f => f.id === id); 355 if (!def || !validators[def.key]) return true; 356 357 const msg = validators[def.key](el ? el.value : ''); 358 if (el) setError(el, msg); 359 return !msg; 360 }; 361 362 const validateAll = () => { 363 let ok = true; 364 fields.forEach(f => { if (!validateFieldById(f.id)) ok = false; }); 365 return ok; 366 }; 367 368 const camposResumen = [ 369 { id: 'nombre', nombre: 'Nombre' }, 370 { id: 'apellidos', nombre: 'Apellidos' }, 371 { id: 'dni', nombre: 'DNI / NIE' }, 372 { id: 'fecha', nombre: 'Fecha de nacimiento' }, 373 { id: 'correo', nombre: 'Correo electrónico' }, 374 { id: 'telefono', nombre: 'Teléfono móvil' }, 375 { id: 'provincia', nombre: 'Provincia' }, 376 { id: 'localidad', nombre: 'Localidad' }, 377 { id: 'postal', nombre: 'Código postal' }, 378 { id: 'direcc', nombre: 'Dirección completa' }, 379 { id: 'foto_dni_delante', nombre: 'DNI anverso' }, 380 { id: 'foto_dni_detras', nombre: 'DNI reverso' }, 381 { id: 'paquete', nombre: 'Tarifa de fibra' }, 382 { id: 'acepta_privacidad', nombre: 'Aceptar la polÃtica de privacidad' } 383 ]; 384 385 const campoEstaCompleto = (id) => { 386 const el = form.querySelector('#' + id); 387 if (!el) return false; 388 389 if (id === 'localidad' && el.disabled) return false; 390 391 if (el.type === 'checkbox') return el.checked; 392 if (el.type === 'file') return !!(el.files && el.files.length); 393 if (el.tagName === 'SELECT') return !!el.value; 394 395 return !!(el.value && el.value.trim() !== ''); 396 }; 397 398 const actualizarProgreso = () => { 399 let completos = 0; 400 401 camposResumen.forEach(campo => { 402 if (campoEstaCompleto(campo.id)) completos++; 403 }); 404 405 const porcentaje = Math.round((completos / camposResumen.length) * 100); 406 407 if (progresoFill) progresoFill.style.width = porcentaje + '%'; 408 if (progresoPorcentaje) progresoPorcentaje.textContent = porcentaje + '%'; 409 }; 410 411 const obtenerCamposPendientes = () => { 412 return camposResumen 413 .filter(campo => !campoEstaCompleto(campo.id)) 414 .map(campo => campo.nombre); 415 }; 416 417 const ocultarResumenErrores = () => { 418 if (!resumenErrores) return; 419 resumenErrores.style.display = 'none'; 420 resumenErrores.innerHTML = ''; 421 }; 422 423 const mostrarResumenErrores = () => { 424 if (!resumenErrores) return []; 425 426 const pendientes = obtenerCamposPendientes();
427 428 if (!pendientes.length) { 429 ocultarResumenErrores(); 430 return []; 431 } 432 433 resumenErrores.innerHTML = ` 434 <strong>Faltan estos campos por completar:</strong> 435 <ul>${pendientes.map(item => `<li>${item}</li>`).join('')}</ul> 436 `; 437 resumenErrores.style.display = 'block'; 438 439 return pendientes; 440 }; 441 442 const actualizarEstadoFlotante = (el) => { 443 const grupo = el.closest('.form-grupo.flotante'); 444 if (!grupo) return; 445 446 if (grupo.classList.contains('fecha-fija')) { 447 if (el.value) { 448 grupo.classList.add('has-value'); 449 } else { 450 grupo.classList.remove('has-value'); 451 } 452 return; 453 } 454 455 if (el.value && el.value.trim() !== '') { 456 grupo.classList.add('has-value'); 457 } else { 458 grupo.classList.remove('has-value'); 459 } 460 }; 461 462 463 464 form.querySelectorAll('.form-grupo.flotante input, .form-grupo.flotante select').forEach(el => { 465 actualizarEstadoFlotante(el); 466 467 const eventos = (el.tagName === 'SELECT') 468 ? ['change', 'blur'] 469 : (el.type === 'date') 470 ? ['input', 'change', 'blur'] 471 : ['input', 'blur']; 472 473 eventos.forEach(evento => { 474 el.addEventListener(evento, () => actualizarEstadoFlotante(el)); 475 }); 476 }); 477 478 const dniEl = form.querySelector('#dni'); 479 if (dniEl) { 480 dniEl.addEventListener('input', () => { 481 dniEl.value = dniEl.value.toUpperCase().replace(/[^0-9A-Z]/g, '').slice(0, 9); 482 }); 483 } 484 485 const telEl = form.querySelector('#telefono'); 486 if (telEl) { 487 telEl.addEventListener('input', () => { 488 telEl.value = telEl.value.replace(/\D/g, '').slice(0, 9); 489 }); 490 } 491 492 const cpEl = form.querySelector('#postal'); 493 if (cpEl) { 494 cpEl.addEventListener('input', () => { 495 cpEl.value = cpEl.value.replace(/\D/g, '').slice(0, 5); 496 }); 497 } 498 499 if (dniEl) { 500 dniEl.addEventListener('blur', () => { 501 dniEl.value = dniEl.value.toUpperCase().replace(/\s+/g, ''); 502 }); 503 } 504 505 const nombreEl = form.querySelector('#nombre'); 506 if (nombreEl) { 507 nombreEl.addEventListener('blur', () => { 508 nombreEl.value = normalizeSpaces(nombreEl.value); 509 }); 510 } 511 512 const apellidosEl = form.querySelector('#apellidos'); 513 if (apellidosEl) { 514 apellidosEl.addEventListener('blur', () => { 515 apellidosEl.value = normalizeSpaces(apellidosEl.value); 516 }); 517 } 518 519 const correoEl = form.querySelector('#correo'); 520 if (correoEl) { 521 correoEl.addEventListener('blur', () => { 522 correoEl.value = (correoEl.value || '').trim().toLowerCase(); 523 }); 524 } 525 526 const direccEl = form.querySelector('#direcc'); 527 if (direccEl) { 528 direccEl.addEventListener('blur', () => { 529 direccEl.value = normalizeSpaces(direccEl.value); 530 }); 531 } 532 533 const cargarLocalidades = async (idProvincia, opciones = {}) => { 534 const {
535 localidadSeleccionada = '', 536 silencioso = false 537 } = opciones; 538 539 if (!provincia || !contLocalidad || !localidad) return; 540 541 contLocalidad.style.display = idProvincia ? 'flex' : 'none'; 542 localidad.disabled = !idProvincia; 543 localidad.value = ''; 544 localidad.innerHTML = '<option value="" disabled selected hidden></option>'; 545 546 actualizarEstadoFlotante(provincia); 547 actualizarEstadoFlotante(localidad); 548 549 if (!silencioso) { 550 validateFieldById('provincia'); 551 validateFieldById('localidad'); 552 actualizarProgreso(); 553 } 554 555 if (!idProvincia) { 556 return; 557 } 558 559 localidad.disabled = true; 560 561 try { 562 const url = `${miFormularioAlta.ajaxUrl}?action=obtener_pueblos_bd&idProvincia=${encodeURIComponent(idProvincia)}`; 563 const res = await fetch(url, { 564 credentials: 'same-origin' 565 }); 566 567 const data = await res.json(); 568 569 localidad.innerHTML = '<option value="" disabled selected hidden></option>'; 570 571 if (Array.isArray(data) && data.length) { 572 data.forEach(item => { 573 const option = document.createElement('option'); 574 option.value = item; 575 option.textContent = item; 576 localidad.appendChild(option); 577 }); 578 579 localidad.disabled = false; 580
581 if (localidadSeleccionada) { 582 const existe = Array.from(localidad.options).some( 583 option => option.value === localidadSeleccionada 584 ); 585 586 if (existe) { 587 localidad.value = localidadSeleccionada; 588 } 589 } 590 } else { 591 localidad.disabled = true; 592 } 593 } catch (e) { 594 localidad.innerHTML = '<option value="" disabled selected hidden></option>'; 595 localidad.disabled = true; 596 } 597 598 actualizarEstadoFlotante(localidad); 599 600 if (!silencioso) { 601 validateFieldById('localidad'); 602 actualizarProgreso(); 603 } 604 }; 605 606 fields.forEach(f => { 607 const el = form.querySelector('#' + f.id); 608 if (!el) return; 609 610 const eventos = (el.type === 'file' || el.tagName === 'SELECT') 611 ? ['change'] 612 : (el.type === 'date') 613 ? ['input', 'change'] 614 : ['input']; 615 616 eventos.forEach(eventName => { 617 el.addEventListener(eventName, () => { 618 validateFieldById(f.id); 619 actualizarProgreso(); 620 621 if (resumenErrores && resumenErrores.style.display !== 'none') { 622 mostrarResumenErrores(); 623 } 624 }); 625 }); 626 627 el.addEventListener('blur', () => { 628 validateFieldById(f.id); 629 actualizarProgreso(); 630 631 if (resumenErrores && resumenErrores.style.display !== 'none') { 632 mostrarResumenErrores(); 633 } 634 }); 635 }); 636 637 if (provincia && contLocalidad && localidad) { 638 contLocalidad.style.display = 'none'; 639 localidad.disabled = true; 640 localidad.value = ''; 641 localidad.innerHTML = '<option value="" disabled selected hidden></option>'; 642 643 provincia.addEventListener('change', async () => { 644 await cargarLocalidades(provincia.value, {
645 localidadSeleccionada: '', 646 silencioso: false 647 }); 648 }); 649 } 650 651 form.addEventListener('change', (e) => { 652 653 const input = e.target; 654 if (!input.classList.contains('archivo-oculto')) return; 655 656 validateFieldById(input.id); 657 658 actualizarProgreso(); 659 660 if (resumenErrores && resumenErrores.style.display !== 'none') { 661 mostrarResumenErrores(); 662 } 663 664 if (!input.files || !input.files[0]) return; 665 666 const reader = new FileReader(); 667 reader.onload = () => { 668 const label = input.nextElementSibling; 669 if (!label) return; 670 671 label.style.backgroundImage = `url(${reader.result})`; 672 label.classList.add('tiene-foto'); 673 674 const texto = label.querySelector('.texto-btn-dni'); 675 if (texto) texto.innerText = 'Cambiar imagen'; 676 }; 677 reader.readAsDataURL(input.files[0]); 678 }); 679 680 681 (async () => { 682 await restaurarBorrador(); 683 })(); 684 685 form.addEventListener('submit', async (e) => { 686 e.preventDefault(); 687 form.classList.add('formulario-validando'); 688 689 const ok = validateAll(); 690 actualizarProgreso(); 691 692 if (!ok) { 693 mostrarResumenErrores(); 694 695 const firstInvalid = fields.map(f => form.querySelector('#' + f.id)).find(el => { 696 const sp = getErrorSpan(el); 697 return sp && sp.textContent.trim().length > 0; 698 }); 699 700 if (firstInvalid) { 701 firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' }); 702 } else if (resumenErrores) { 703 resumenErrores.scrollIntoView({ behavior: 'smooth', block: 'center' }); 704 } 705 706 return; 707 } 708 709 const captchaResponse = (typeof grecaptcha !== 'undefined') 710 ? grecaptcha.getResponse() 711 : ''; 712 713 if (!captchaResponse) { 714 if (resumenErrores) { 715 resumenErrores.innerHTML = '<strong>Error:</strong><ul><li>Por favor, confirma que no eres un robot.</li></ul>'; 716 resumenErrores.style.display = 'block'; 717 resumenErrores.scrollIntoView({ behavior: 'smooth', block: 'center' }); 718 } 719 return; 720 } 721 722 ocultarResumenErrores(); 723 activarEstadoEnvio(); 724 725 const fd = new FormData(form); 726 fd.append('action', 'procesar_formulario_alta'); 727 fd.set('g-recaptcha-response', captchaResponse); 728 729 try { 730 const res = await fetch(miFormularioAlta.ajaxUrl, { 731 method: 'POST', 732 body: fd, 733 credentials: 'same-origin' 734 }); 735 736 const text = await res.text(); 737 console.log('RESPUESTA DEL SERVIDOR:', text); 738 739 if (text.includes('success')) { 740 limpiarBorrador(); 741 742 desactivarEstadoEnvio(); 743 744 const intro = form.parentElement?.querySelector('.intro-formulario-alta'); 745 if (intro) { 746 intro.style.display = 'none'; 747 } 748 749 750 751 form.innerHTML = ` 752 <div style="text-align:center; padding:40px 20px;"> 753 <div style="width:80px;height:80px;background:#27ae60;color:white;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:40px;margin:0 auto 18px;box-shadow:0 4px 12px rgba(39,174,96,.3);">â</div> 754 <h2 style="color:#2c3e50;font-size:22px;font-weight:700;margin:0;font-family:'Segoe UI', Roboto, sans-serif;">${miFormularioAlta.successText}</h2> 755 </div>`; 756 } else { 757 const codigo = text.trim(); 758 759 if (codigo === 'captcha_error' && typeof grecaptcha !== 'undefined') { 760 try { 761 grecaptcha.reset(); 762 } catch (e) {} 763 } 764 765 const mensajesServidor = { 766 nonce_error: 'La sesión del formulario ha caducado. Recarga la página e inténtalo de nuevo.', 767 error_campos_vacios: 'Hay campos obligatorios sin completar.', 768 privacidad_error: 'Debes aceptar la polÃtica de privacidad.', 769 nombre_error: 'Revisa el nombre introducido.', 770 apellidos_error: 'Revisa los apellidos introducidos.', 771 dni_error: 'El DNI/NIE no es válido o la letra no coincide.', 772 fecha_error: 'La fecha de nacimiento no es válida o no cumple la mayorÃa de edad.',
773 correo_error: 'El correo electrónico no es válido.', 774 correo_temporal_error: 'No se permiten correos temporales o desechables.', 775 telefono_error: 'El teléfono debe tener 9 dÃgitos y empezar por 6, 7, 8 o 9.', 776 cp_error: 'El código postal no es válido.', 777 cp_provincia_error: 'El código postal no coincide con la provincia seleccionada.', 778 direccion_error: 'La dirección no es válida.', 779 paquete_error: 'Debes seleccionar una tarifa.', 780 localidad_error: 'La localidad no corresponde con la provincia seleccionada.', 781 archivo_delante_error: 'La imagen del DNI anverso debe ser JPG, PNG o WEBP y no superar 5 MB.', 782 archivo_detras_error: 'La imagen del DNI reverso debe ser JPG, PNG o WEBP y no superar 5 MB.', 783 error_archivos: 'Debes adjuntar ambas imágenes del documento.', 784 captcha_error: 'No se pudo validar el reCAPTCHA. Vuelve a marcarlo.', 785 duplicado_error: 'Ya existe una solicitud pen
785diente con ese DNI/NIE, correo o teléfono.', 786 upload_error_1: 'No se pudo subir la imagen del DNI anverso.', 787 upload_error_2: 'No se pudo subir la imagen del DNI reverso.', 788 provincia_error: 'La provincia seleccionada no es válida.', 789 demasiados_intentos: 'Has realizado demasiados intentos en poco tiempo. Espera 15 minutos y vuelve a intentarlo.' 790 }; 791 792 const campoPorCodigo = { 793 privacidad_error: 'acepta_privacidad', 794 nombre_error: 'nombre', 795 apellidos_error: 'apellidos', 796 dni_error: 'dni', 797 fecha_error: 'fecha', 798 correo_error: 'correo', 799 correo_temporal_error: 'correo', 800 telefono_error: 'telefono', 801 cp_error: 'postal', 802 cp_provincia_error: 'postal', 803 provincia_error: 'provincia', 804 direccion_error: 'direcc', 805 paquete_error: 'paquete', 806 localidad_error: 'localidad', 807 archivo_delante_error: 'foto_dni_delante', 808 archivo_detras_error: 'foto_dni_detras' 809 }; 810 811 const mensaje = mensajesServidor[codigo] || 'Faltan estos campos por completar'; 812 const campoId = campoPorCodigo[codigo]; 813 814 form.classList.add('formulario-validando'); 815 816 if (campoId) { 817 const el = form.querySelector('#' + campoId); 818 if (el) { 819 setError(el, mensaje); 820 el.reportValidity?.(); 821 el.scrollIntoView({ behavior: 'smooth', block: 'center' }); 822 } 823 } 824 825 if (resumenErrores) { 826 mostrarResumenErrores(); 827 828 if (!campoId) { 829 resumenErrores.innerHTML = `<strong>Error:</strong><ul><li>${mensaje}</li></ul>`; 830 resumenErrores.style.display = 'block'; 831 resumenErrores.scrollIntoView({ behavior: 'smooth', block: 'center' }); 832 } 833 } 834 835 desactivarEstadoEnvio(); 836 } 837 } catch (err) { 838 desactivarEstadoEnvio(); 839 840 form.classList.add('formulario-validando'); 841 842 if (typeof grecaptcha !== 'undefined') { 843 try { 844 grecaptcha.reset(); 845 } catch (e) {} 846 } 847 848 if (resumenErrores) { 849 resumenErrores.innerHTML = `<strong>Error:</strong><ul><li>No se ha podido enviar el formulario por un problema de conexión. Inténtalo de nuevo.</li></ul>`; 850 resumenErrores.style.display = 'block'; 851 resumenErrores.scrollIntoView({ behavior: 'smooth', block: 'center' }); 852 } 853 854 if (btnEnviar) { 855 btnEnviar.innerText = 'ENVIAR SOLICITUD DE ALTA'; 856 btnEnviar.disabled = false; 857 } 858 } 859 }); 860}); 861 862 863window.miRecaptchaExpirado = function () { 864 const form = document.getElementById('form-alta-final'); 865 if (form && form.dataset.enviando === '1') { 866 return; 867 } 868 869 const resumenErrores = document.getElementById('resumen-errores-formulario'); 870 if (resumenErrores) { 871 resumenErrores.innerHTML = '<strong>Error:</strong><ul><li>El reCAPTCHA ha caducado. Vuelve a marcarlo.</li></ul>'; 872 resumenErrores.style.display = 'block'; 873 } 874}; 875 876window.miRecaptchaError = function () { 877 const form = document.getElementById('form-alta-final'); 878 if (form && form.dataset.enviando === '1') { 879 return; 880 } 881 882 if (typeof grecaptcha !== 'undefined') { 883 try { grecaptcha.reset(); } catch (e) {} 884 } 885 886 const resumenErrores = document.getElementById('resumen-errores-formulario'); 887 if (resumenErrores) { 888 resumenErrores.innerHTML = '<strong>Error:</strong><ul><li>No se pudo cargar o validar el reCAPTCHA. Inténtalo de nuevo.</li></ul>'; 889 resumenErrores.style.display = 'block'; 890 resumenErrores.scrollIntoView({ behavior: 'smooth', block: 'center' }); 891 } 892};
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.