PageSourceSearch

https://ciberica.es/wp-content/plugins/mejora-form-ciberica/assets/js/form-alta-ciberica.js?ver=1.4

js ciberica.es collected 2026-10-08 14:55:31 UTC 31,119 bytes, 892 lines download raw bytes

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.