1// ========================= 2// Perfil 3// ========================= 4 5// Variables globales para cache 6let userProfileData = null; 7let countriesData = null; 8 9// Sistema de cache con localStorage 10const PROFILE_CACHE_KEY = 'userProfile'; 11const PROFILE_TIMESTAMP_KEY = 'userProfileTimestamp'; 12const CACHE_DURATION = 15 * 60 * 1000; // 15 minutos 13//const CACHE_DURATION = 15; // PARA TESTEO 14 15// ========================= 16// Funciones de cache con localStorage 17// ========================= 18 19// Función para obtener datos del localStorage 20function getUserProfileFromStorage() { 21 try { 22 const cached = localStorage.getItem(PROFILE_CACHE_KEY); 23 const timestamp = localStorage.getItem(PROFILE_TIMESTAMP_KEY); 24 25 if (cached && timestamp) { 26 const age = Date.now() - parseInt(timestamp); 27 28 if (age < CACHE_DURATION) { 29 console.log('Usando datos de perfil desde localStorage'); 30 return JSON.parse(cached); 31 } else { 32 console.log('Cache de perfil expirado, limpiando...'); 33 clearUserProfileStorage(); 34 } 35 } 36 } catch (error) { 37 console.error('Error leyendo cache de perfil:', error); 38 clearUserProfileStorage(); 39 } 40 41 return null; 42} 43 44// Función para guardar datos en localStorage 45function saveUserProfileToStorage(data) { 46 try { 47 localStorage.setItem(PROFILE_CACHE_KEY, JSON.stringify(data)); 48 localStorage.setItem(PROFILE_TIMESTAMP_KEY, Date.now().toString()); 49 console.log('Datos de perfil guardados en localStorage'); 50 } catch (error) { 51 console.error('Error guardando cache de perfil:', error); 52 } 53} 54 55// Función para limpiar cache 56function clearUserProfileStorage() { 57 localStorage.removeItem(PROFILE_CACHE_KEY); 58 localStorage.removeItem(PROFILE_TIMESTAMP_KEY); 59} 60 61// Función para actualizar cache después de cambios 62function updateUserProfileCache(updatedFields) { 63 const current = getUserProfileFromStorage(); 64 if (current) { 65 const updated = { ...current, ...updatedFields }; 66 saveUserProfileToStorage(updated); 67 userProfileData = updated; // Actualizar también memoria 68 renderizarSaludo(updated); // 2026-09-18 - si cambio el nombre 69 console.log('Cache de perfil actualizado con nuevos datos'); 70 } 71} 72 73// ========================= 74// Funciones para Update Screen (solo perfil) 75// ========================= 76function showUpdateScreen(type) { 77 const main = document.getElementById('mainScreen'); 78 if (!main) return; // evita error si no existe 79 80 // Ocultar pantalla principal 81 main.classList.add('hidden'); 82 83 // Ocultar todas las pantallas de update 84 const screens = document.querySelectorAll('.update-screen'); 85 screens.forEach(screen => screen.classList.remove('active')); 86 87 // Mostrar la pantalla seleccionada 88 const screenId = type + 'Screen'; 89 const target = document.getElementById(screenId); 90 if (target) target.classList.add('active'); 91 92 // Hook: cargar datos especÃficos según el tipo de pantalla 93 if (screenId === 'passwordScreen') { 94 initPasswordScreenValidation(); 95 } else if (screenId === 'personalScreen') { 96 loadPersonalData(); // Cargar datos personales 97 } 98} 99 100// 2026-09-18 - Solapas del profile (Mi cuenta / Preguntas frecuentes / Contactanos), como account.forbes.com. 101// Cambian sin recargar; el hash (#contactanos) deja linkear cada una desde afuera 102const PROFILE_TABS = ['mi-cuenta', 'preguntas-frecuentes', 'contactanos']; 103 104function mostrarSolapa(tab) { 105 if (PROFILE_TABS.indexOf(tab) === -1) tab = 'mi-cuenta'; 106 document.querySelectorAll('.profile-tab-link').forEach(a => { 107 a.classList.toggle('is-active', a.dataset.tab === tab); 108 a.setAttribute('aria-current', a.dataset.tab === tab ? 'page' : 'false'); 109 }); 110 document.querySelectorAll('.profile-tab-panel').forEach(p => { 111 p.classList.toggle('is-active', p.dataset.tab === tab); 112 });
113} 114 115function initSolapas() { 116 if (!document.querySelector('.profile-tabs')) return; 117 118 // Links de las solapas y links internos (data-tab-link) dentro del contenido 119 document.addEventListener('click', (e) => { 120 const a = e.target.closest('.profile-tab-link, [data-tab-link]'); 121 if (!a) return; 122 const tab = a.dataset.tab || a.dataset.tabLink; 123 if (PROFILE_TABS.indexOf(tab) === -1) return; 124 e.preventDefault(); 125 history.replaceState(null, '', '#' + tab); 126 mostrarSolapa(tab); 127 if (tab === 'mi-cuenta') showMainScreen(); 128 window.scrollTo(0, 0); 129 }); 130 131 // Links desde afuera o el "Atras" del navegador 132 window.addEventListener('hashchange', () => mostrarSolapa(location.hash.slice(1))); 133 134 // Si viene del checkout con perfil incompleto, siempre Mi cuenta 135 const completar = new URLSearchParams(window.location.search).has('completar'); 136 mostrarSolapa(completar ? 'mi-cuenta' : location.hash.slice(1)); 137} 138 139// 2026-09-18 - "¡Hola, Nombre!" en el encabezado. Si el nombre quedo como el email (cuentas creadas 140// sin displayName), va el saludo sin nombre 141function renderizarSaludo(user) { 142 const h = document.getElementById('profileSaludo'); 143 if (!h) return; 144 const nombre = (user && user.nombre ? String(user.nombre) : '').trim(); 145 h.textContent = (nombre && nombre.indexOf('@') === -1) ? '¡Hola, ' + nombre + '!' : '¡Hola!'; 146} 147 148// 2026-09-18 - Precarga el formulario de Contactanos (Cognito, embed seamless) con los datos del perfil. 149// Las claves son los "Field Name" de Cognito, puestos a mano en ingles (Developer Mode > Customize Field 150// Names): por defecto salen de la etiqueta y conservan las tildes ("Teléfono"). El ID de usuario es un 151// campo oculto: con eso se lo encuentra directo en el panel de Boxful 152function prefillContacto(user) { 153 if (!user || !document.getElementById('cognitoForm')) return; 154 const nombre = (user.nombre || '').indexOf('@') === -1 ? (user.nombre || '') : ''; 155 const datos = { 156 Name: { First: nombre, Last: user.apellido || '' }, 157 Email: firebase.auth().currentUser ? firebase.auth().currentUser.email : (user.email || ''), 158 Phone: user.telefono || '', 159 UserId: String(user.id || '') 160 }; 161 // Cognito.prefill existe cuando termino de cargar seamless.js 162 const intentar = (n) => { 163 if (window.Cognito && typeof window.Cognito.prefill === 'function') { 164 window.Cognito.prefill(datos); 165 } else if (n > 0) { 166 setTimeout(() => intentar(n - 1), 500); 167 } 168 }; 169 intentar(20); 170} 171 172function showMainScreen() { 173 const main = document.getElementById('mainScreen'); 174 if (!main) return; 175 176 // Mostrar pantalla principal 177 main.classList.remove('hidden'); 178 179 // Ocultar todas las pantallas de update 180 const screens = document.querySelectorAll('.update-screen'); 181 screens.forEach(screen => screen.classList.remove('active')); 182} 183 184// ========================= 185// Funciones para manejo de datos del usuario 186// ========================= 187 188// Función principal para obtener datos (con cache localStorage) 189async function getUserProfileData(forceRefresh = false) { 190 // Si no fuerza refresh, intentar obtener del localStorage 191 if (!forceRefresh) { 192 const cached = getUserProfileFromStorage(); 193 if (cached) { 194 userProfileData = cached; // Mantener también en memoria para la sesión 195 console.log(userProfileData); 196 renderizarSaludo(cached); // 2026-09-18 197 prefillContacto(cached); // 2026-09-18 198 renderizarSuscripciones(cached); 199 return cached; 200 } 201 } 202 203 console.log('Obteniendo datos de perfil desde servidor...'); 204 const result = await window.firebaseApi.obtenerDatosUsuario(); 205 206 if (result.success) { 207 userProfileData = result.user; 208 saveUserProfileToStorage(result.user); // Guardar en localStorage 209 renderizarSaludo(result.user); // 2026-09-18 210 prefillContacto(result.user); // 2026-09-18 211 renderizarSuscripciones(result.user); 212 return result.user; 213 } 214 215 return null; 216} 217
218async function renderizarSuscripciones(resultado) { 219 // Contenedor principal 220 const suscriptionCard = document.getElementById('suscription_type'); 221 222 // Elementos del DOM 223 const content = suscriptionCard.querySelector('.suscription_content'); 224 const subscriptionMessage = suscriptionCard.querySelector('.subscription-message'); 225 const subscriptionButtons = suscriptionCard.querySelector('.subscription-buttons'); 226 const suscriptionText = suscriptionCard.querySelector('.suscription-text'); 227 const suscriptionWrap = suscriptionCard.querySelector('.suscription-wrap'); 228 const expireSpan = suscriptionCard.querySelector('.expire'); 229 const subscriptionActions = suscriptionCard.querySelector('.subscription-actions'); 230 231 232 // Ocultar todo por defecto 233 subscriptionMessage.style.display = 'none'; 234 subscriptionButtons.style.display = 'none'; 235 suscriptionText.style.display = 'none'; 236 if (subscriptionActions) subscriptionActions.style.display = 'none'; 237 238 content.style.display = 'block'; 239 240 if (!resultado) { 241 // Sin sesión o error - mostrar botones 242 subscriptionMessage.style.display = 'block'; 243 subscriptionButtons.style.display = 'flex'; 244 return; 245 } 246 247 const user = resultado; 248 const plan = user.plan; 249 250 // Usuario sin plan activo 251 if (!plan || !user.has_access) { 252 subscriptionMessage.style.display = 'block'; 253 subscriptionButtons.style.display = 'flex'; 254 return; 255 } 256 257 // Usuario con plan provider "amura" 258 if (plan.provider === 'amura') { 259 suscriptionText.style.display = 'flex'; 260 261 // Formatear y actualizar fecha de vencimiento 262 const fechaVencimiento = formatearFecha(plan.fecha); 263 expireSpan.textContent = `Vence el ${fechaVencimiento}`; 264 265 // Actualizar empresa 266 const empresa = plan.string || 'Amura Platform'; 267 const empresaSpan = suscriptionWrap.querySelector('span'); 268 empresaSpan.textContent = `PROPORCIONADO POR ${empresa.toUpperCase()}`; 269 270 return; 271 } 272 273 // Usuario con plan de otro provider (no amura) 274 suscriptionText.style.display = 'flex'; 275 276 // Ocultar elementos especÃficos para providers no-amura 277 suscriptionWrap.style.display = 'none'; 278 expireSpan.style.display = 'none'; 279 280 // Mostrar botones de acción para suscripciones no-amura 281 if (subscriptionActions) { 282 subscriptionActions.style.display = 'flex'; 283 284 // 2026-09-18 - Bloque 3: un solo manejador para los cuatro botones al portal de Boxful (initBotonesPortal). 285 // Lo de abajo sumaba un listener cada vez que se renderizaba (al cargar y al abrir "Información personal"): 286 // un click podia abrir dos pestañas. Ademas usaba window.open + confirm + alert 287 initBotonesPortal(subscriptionActions); 288 return; 289 290 // Configurar el botón de cancelar 291 const cancellink = subscriptionActions.querySelector('.subscription-link'); 292 if (cancellink) { 293 cancellink.addEventListener('click', async(e)=> { 294 e.preventDefault(); 295 296 if (confirm('¿Estás seguro que deseas cancelar tu suscripción?')) { 297 // Guardar texto original 298 const originalText = cancellink.textContent; 299 300 // Deshabilitar y mostrar estado de carga 301 cancellink.style.pointerEvents = 'none'; 302 cancellink.style.opacity = '0.6'; 303 cancellink.textContent = 'Redirigiendo...'; 304 305 try { 306 await cancelarSuscripcion(user); 307 } catch (error) { 308 console.log('Error al cancelar la suscripción. Intenta nuevamente.'); 309 } 310 cancellink.textContent = originalText; 311 cancellink.style.pointerEvents = 'auto'; 312 cancellink.style.opacity = '1'; 313 } 314 }); 315 } 316 317 // Configurar el link de facturas 318 const invoicesLink = subscriptionActions.querySelector('.invoices-link'); 319 if (invoicesLink) { 320 invoicesLink.addEventListener('click', async(e)=> { 321 e.preventDefault(); 322 323 const user = firebase.auth().currentUser; 324 if (!user) { 325 alert('Usuario no autenticado'); 326 return; 327 } 328 329 // Guardar estado original 330 const originalText = invoicesLink.textContent; 331 332 // Mostrar estado de carga 333 invoicesLink.style.pointerEvents = 'none'; 334 invoicesLink.style.opacity = '0.6'; 335 invoicesLink.textContent = 'Redirigiendo...'; 336 337 try { 338 const idToken = await user.getIdToken(true); 339 340 const response = await fetch(url_root + 'api/users/invoice', { 341 method: 'POST', 342 headers: { 343 'Content-Type': 'application/json', 344 'X-Firebase-IdToken': idToken, 345 'Authorization': 'Bearer ' + idToken 346 }, 347 body: JSON.stringify({ uid: user.firebase_uid }) 348 }); 349 350 const result = await response.json().catch(() => ({})); 351 352 if (response.ok && result.ok) { 353 const newWindow = window.open(result.url, '_blank'); 354 355 // Si fue bloqueado, mostrar mensaje y ofrecer alternativa 356 if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { 357 if (confirm('Los pop-ups están bloqueados.\n¿Querés abrir en esta misma página?')) { 358 window.location.href = result.url; 359 } else { 360 alert('Por favor, habilitá los pop-ups y volvé a intentar'); 361 } 362 } 363 } else { 364 throw new Error('Error al obtener facturas'); 365 } 366 } catch (error) { 367 console.error('Error al invoice:', error); 368 console.error('No se pudieron obtener las facturas. Intentá nuevamente.'); 369 } finally { 370 // Restaurar estado 371 invoicesLink.style.pointerEvents = 'auto'; 372 invoicesLink.style.opacity = '1'; 373 invoicesLink.textContent = originalText; 374 } 375 }); 376 } 377 } 378} 379 380// Función para cancelar suscripción 381// 2026-09-18 - Bloque 3: botones al portal de Boxful. Cada uno pide a nuestra API la URL con el magic link 382// (msgver) y va en la misma pestaña: con window.open despues del fetch el navegador lo bloqueaba. 383// "suscripciones" ->
383 api/users/cancel devuelve /membership/subscriptions (medio de pago, plan y baja). 384// "facturas" -> api/users/invoice devuelve /membership/invoices. Sin cambios en el core. 385const PORTAL_ENDPOINTS = { 386 suscripciones: 'api/users/cancel', 387 facturas: 'api/users/invoice' 388}; 389 390function initBotonesPortal(contenedor) { 391 if (!contenedor || contenedor.dataset.portalListo) return; // una sola vez, aunque se renderice de nuevo 392 contenedor.dataset.portalListo = '1'; 393 394 const mensaje = document.querySelector('#suscription_type .portal-mensaje'); 395 396 contenedor.addEventListener('click', async (e) => { 397 const link = e.target.closest('[data-portal]'); 398 if (!link) return; 399 e.preventDefault(); 400 401 const endpoint = PORTAL_ENDPOINTS[link.dataset.portal]; 402 const user = firebase.auth().currentUser; 403 if (!endpoint || !user) return; 404 405 if (mensaje) mensaje.hidden = true; 406 const textoOriginal = link.textContent; 407 link.style.pointerEvents = 'none'; 408 link.style.opacity = '0.6'; 409 link.textContent = 'Redirigiendo...'; 410 411 let status = 0; 412 try { 413 const idToken = await user.getIdToken(true); 414 const response = await fetch(url_root + endpoint, { 415 method: 'POST', 416 headers: { 417 'Content-Type': 'application/json', 418 'X-Firebase-IdToken': idToken, 419 'Authorization': 'Bearer ' + idToken 420 }, 421 body: JSON.stringify({ uid: user.uid }) 422 }); 423 status = response.status; 424 const result = await response.json().catch(() => ({})); 425 if (response.ok && result.ok && result.url) { 426 window.location.href = result.url; 427 return; 428 } 429 console.error('[Portal] Error:', result.error || result.message || status); 430 } catch (error) { 431 console.error('[Portal] Error:', error); 432 } 433 434 // Mismo criterio que el checkout: texto para el suscriptor y un codigo para identificarlo (CK{status}, CK0 sin respuesta) 435 if (mensaje) { 436 mensaje.textContent = 'No pudimos abrir la gestión de tu suscripción. Volvé a intentarlo en unos minutos. (código CK' + status + ')'; 437 mensaje.hidden = false; 438 } 439 link.textContent = textoOriginal; 440 link.style.pointerEvents = 'auto'; 441 link.style.opacity = '1'; 442 }); 443 444 // Al volver con "Atras" desde Boxful la pagina sale del cache del navegador con el boton en "Redirigiendo..." 445 window.addEventListener('pageshow', (e) => { 446 if (!e.persisted) return; 447 contenedor.querySelectorAll('[data-portal]').forEach(a => { 448 a.style.pointerEvents = 'auto'; 449 a.style.opacity = '1'; 450 }); 451 location.reload(); 452 }); 453} 454 455async function cancelarSuscripcion(user) { 456 try { 457 const user = firebase.auth().currentUser; 458 if (!user) { 459 throw new Error('Usuario no autenticado'); 460 } 461 const idToken = await user.getIdToken(true); 462 463 const response = await fetch(url_root + 'api/users/cancel', { 464 method: 'POST', 465 headers: { 466 'Content-Type': 'application/json', 467 'X-Firebase-IdToken': idToken, 468 'Authorization': 'Bearer ' + idToken 469 }, 470 body: JSON.stringify({ uid: user.firebase_uid }) 471 }); 472 const result = await response.json().catch(() => ({})); 473 if (response.ok) { 474 475 if (result.ok) { 476 // 477 const newWindow = window.open(result.url, '_blank'); 478 479 // Si fue bloqueado, mostrar mensaje y ofrecer alternativa 480 if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { 481 if (confirm('Los pop-ups están bloqueados.\n¿Querés abrir en esta misma página?')) { 482 window.location.href = result.url; 483 } else { 484 alert('Por favor, habilitá los pop-ups y volvé a intentar'); 485 } 486 } 487 } 488 } else { 489 alert('Error al cancelar la suscripción'); 490 } 491 } catch (error) { 492 console.error('Error cancelando suscripción:', error); 493 alert('Error al cancelar la suscripción'); 494 } 495} 496 497function formatearFecha(fechaString) { 498 if (!fechaString) return ''; 499 500 const fecha = new Date(fechaString); 501 const dia = fecha.getDate().toString().padStart(2, '0'); 502 const mes = (fecha.getMonth() + 1).toString().padStart(2, '0'); 503 const anio = fecha.getFullYear(); 504 505 return `${dia}/${mes}/${anio}`; 506} 507 508// ========================= 509// Funciones de validación visual 510// ========================= 511 512// Función para validación visual de campos individuales 513function validateField(field, showValidation = true) { 514 const fieldName = field.name; 515 const value = field.value.trim(); 516 const wrapper = field.closest('.input-wrapper'); 517 const icon = wrapper ? wrapper.querySelector('.validation-icon') : null; 518 519 let isValid = false; 520 521 // Validaciones especÃficas por campo 522 switch(fieldName) { 523 case 'nombre': 524 case 'apellido': 525 case 'address_street': 526 case 'address_number': 527 case 'city_text': 528 case 'postal_code': 529 isValid = value.length > 0;
530 break; 531 case 'telefono': 532 isValid = value.length >= 5; 533 break; 534 case 'doc_type': 535 case 'country_code': 536 case 'province_id': 537 isValid = value.length > 0; 538 break; 539 case 'doc_number': 540 isValid = value.length >= 3; 541 break; 542 // 2026-09-18 - Fecha de nacimiento: AAAA-MM-DD (lo que da el input date), entre 1900 y hoy 543 case 'birth_date': 544 isValid = /^\d{4}-\d{2}-\d{2}$/.test(value) && value >= '1900-01-01' && value <= new Date().toISOString().slice(0, 10); 545 break; 546 default: 547 isValid = true; // Otros campos son opcionales 548 } 549 550 // Solo aplicar clases visuales si showValidation es true 551 if (wrapper && icon && showValidation) { 552 if (value === '') { 553 // Campo vacÃo - sin validación visual 554 field.classList.remove('valid', 'invalid'); 555 icon.classList.remove('valid', 'invalid'); 556 } else if (isValid) { 557 // Campo válido 558 field.classList.add('valid'); 559 field.classList.remove('invalid'); 560 icon.classList.add('valid'); 561 icon.classList.remove('invalid'); 562 } else { 563 // Campo inválido 564 field.classList.add('invalid'); 565 field.classList.remove('valid'); 566 icon.classList.add('invalid'); 567 icon.classList.remove('valid'); 568 } 569 } 570 571 return isValid; 572} 573 574// Función actualizada para validar todo el formulario (solo lógica, sin mostrar validación visual) 575function validatePersonalFormLogic() { 576 const form = document.querySelector('#personalScreen form'); 577 if (!form) return; 578 579 const saveBtn = form.querySelector('.btn-save'); 580 if (!saveBtn) return; 581 582 // 2025-03-04 - Campos obligatorios para envÃo de revista 583 // 2026-07-28 - Se suma telefono como obligatorio 584 const requiredFields = [ 585 // 2026-09-18 - Se suma birth_date como obligatorio 586 'nombre', 'apellido', 'telefono', 'doc_type', 'doc_number', 'birth_date', 587 'address_street', 'address_number', 588 'country_code', 'province_id', 'city_text', 'postal_code' 589 ]; 590 591 let isFormValid = true; 592 593 requiredFields.forEach(fieldName => { 594 const field = form.querySelector(`[name="${fieldName}"]`); 595 if (field) { 596 const valid = validateField(field, false); 597 if (!valid || !field.value.trim()) { 598 isFormValid = false; 599 } 600 } 601 }); 602 603 // Habilitar/deshabilitar botón 604 if (isFormValid) { 605 saveBtn.classList.remove('is-disabled'); 606 } else { 607 saveBtn.classList.add('is-disabled'); 608 } 609} 610 611// Función actualizada para validar todo el formulario (con validación visual) 612function validatePersonalForm() { 613 const form = document.querySelector('#personalScreen form'); 614 if (!form) return; 615 616 const saveBtn = form.querySelector('.btn-save'); 617 if (!saveBtn) return; 618 619 // 2025-03-04 - Mismos campos obligatorios que validatePersonalFormLogic 620 const requiredFields = [ 621 // 2026-09-18 - Se suma birth_date como obligatorio 622 'nombre', 'apellido', 'telefono', 'doc_type', 'doc_number', 'birth_date', 623 'address_street', 'address_number', 624 'country_code', 'province_id', 'city_text', 'postal_code' 625 ]; 626 627 let isFormValid = true; 628 629 requiredFields.forEach(fieldName => { 630 const field = form.querySelector(`[name="${fieldName}"]`); 631 if (field) { 632 const valid = validateField(field, false); // Sin mostrar validación al cargar 633 if (!valid || !field.value.trim()) { 634 isFormValid = false; 635 } 636 } 637 }); 638 639 // Habilitar/deshabilitar botón 640 if (isFormValid) { 641 saveBtn.classList.remove('is-disabled'); 642 } else { 643 saveBtn.classList.add('is-disabled'); 644 } 645} 646 647// Función para cargar datos en el formulario de información personal (actualizada) 648async function loadPersonalData() { 649 const userData = await getUserProfileData(); 650 if (!userData) return; 651 652 const form = document.querySelector('#personalScreen form'); 653 if (!form) return; 654 655 // Mapear los datos incluyendo newsletter 656 const fieldMap = { 657 'nombre': userData.nombre || '', 658 'apellido': userData.apellido || '', 659 'telefono': userData.telefono || '', 660 'doc_type': userData.doc_type || '', 661 'doc_number': userData.doc_number || '', 662 'birth_date': userData.birth_date || '', // 2026-09-18 663 'address_street': userData.address_street || '', 664 'address_number': userData.address_number || '', 665 'address_floor': userData.address_floor || '', 666 'address_notes': userData.address_notes || '', 667 'country_code': userData.country_code || '', 668 'province_id': userData.province_id || '', 669 'city_text': userData.city_text || '', 670 'postal_code': userData.postal_code || '', 671 //'extras': userData.extras || '', 672 'newsletter': userData.newsletter // Para checkbox 673 }; 674 675 // Cargar paÃses en el select PRIMERO (antes de llenar valores) 676 await loadCountries(); 677 678 // Llenar los campos del formulario (incluyendo country_code y newsletter)
679 Object.keys(fieldMap).forEach(fieldName => { 680 const field = form.querySelector(`[name="${fieldName}"]`); 681 if (field) { 682 if (field.type === 'checkbox') { 683 // Para checkbox, verificar si el valor es 1 o true 684 field.checked = (fieldMap[fieldName] == 1 || fieldMap[fieldName] === true); 685 } else if (fieldName === 'extras') { 686 // Manejar campo JSON extras 687 let extrasValue = ''; 688 if (fieldMap[fieldName]) { 689 try { 690 const extrasJson = JSON.parse(fieldMap[fieldName]); 691 // Si tiene la estructura {"notes": "contenido"}, extraer solo las notas 692 if (extrasJson && typeof extrasJson === 'object' && extrasJson.notes) { 693 extrasValue = extrasJson.notes; 694 } else { 695 // Si es otro JSON, convertir a string legible 696 extrasValue = JSON.stringify(extrasJson, null, 2); 697 } 698 } catch (e) { 699 // Si no es JSON válido, usar tal como está 700 extrasValue = fieldMap[fieldName]; 701 } 702 } 703 field.value = extrasValue; 704 } else { 705 field.value = fieldMap[fieldName]; 706 } 707 } 708 }); 709 710 // Si hay un paÃs seleccionado, cargar sus provincias 711 if (userData.country_code) { 712 await loadProvinces(userData.country_code, userData.province_id); 713 } else { 714 // Si no hay paÃs, asegurar que provincias esté deshabilitado y vacÃo 715 const provinceSelect = document.querySelector('#province_id'); 716 if (provinceSelect) { 717 provinceSelect.disabled = true; 718 provinceSelect.innerHTML = '<option value="">Seleccione provincia</option>'; 719 } 720 } 721 722 // Ejecutar validación inicial 723 validatePersonalForm(); 724 725 // â AGREGAR AQUà - Actualizar contador después de cargar datos 726 initTextareaCounter(); 727} 728 729// Función para guardar cambios en información personal (actualizada con cache) 730async function savePersonalData(formData) { 731 try { 732 const user = firebase.auth().currentUser; 733 if (!user) { 734 throw new Error('Usuario no autenticado'); 735 } 736 737 const idToken = await user.getIdToken(true); 738 739 const updateData = { 740 uid: user.uid, 741 ...formData 742 }; 743 744 const response = await fetch(url_root + 'api/users/update', { 745 method: 'POST', 746 headers: { 747 'Content-Type': 'application/json', 748 'X-Firebase-IdToken': idToken, 749 'Authorization': 'Bearer ' + idToken 750 }, 751 body: JSON.stringify(updateData) 752 }); 753 754 const result = await response.json(); 755 756 if (response.ok && result.ok) { 757 // Actualizar cache local con los nuevos datos 758 updateUserProfileCache(formData); 759 760 return { ok: true, message: result.message || 'Datos actualizados correctamente.' }; 761 } else { 762 return { ok: false, message: result.error || 'Error al actualizar datos.' }; 763 } 764 765 } catch (error) { 766 console.error('Error guardando datos personales:', error); 767 return { ok: false, message: 'Error inesperado. Inténtalo de nuevo.' }; 768 } 769} 770 771// 2026-09-18 - Pide el checkout de Boxful para el plan que apreto en /suscribe (mismo endpoint que usuarios.checkout.js) 772async function irAlPago(planId) { 773 try { 774 const user = firebase.auth().currentUser; 775 if (!user) { 776 throw new Error('Usuario no autenticado'); 777 } 778 779 const idToken = await user.getIdToken(true); 780 781 const response = await fetch(url_root + 'api/users/checkout', { 782 method: 'POST', 783 headers: { 784 'Content-Type': 'application/json', 785 'X-Firebase-IdToken': idToken, 786 'Authorization': 'Bearer ' + idToken 787 }, 788 body: JSON.stringify({ 789 plan: planId, 790 tenant: firebase.auth().tenantId || null 791 }) 792 }); 793 794 const result = await response.json().catch(() => ({})); 795 796 if (response.ok && result.checkoutUrl) { 797 return { ok: true, checkoutUrl: result.checkoutUrl }; 798 } 799 // El error de Boxful trae http_code y respuesta cruda: a la consola, no al suscriptor 800 const detalle = result.error || result.message || '';
801 console.error('[Checkout] Error desde el perfil:', detalle); 802 // Mismos codigos que usuarios.checkout.js: BX{http} lo rechazo Boxful, CK{status} nuestra API, CK0 sin respuesta 803 const bx = /Boxful HTTP (\d+)/.exec(detalle); 804 const codigo = ' (código ' + (bx ? 'BX' + bx[1] : 'CK' + response.status) + ')'; 805 if (/HTTP 422/.test(detalle) && /email/i.test(detalle)) { 806 return { ok: false, message: 'Tus datos se guardaron, pero ya hay una cuenta de pago registrada con tu email. Escribinos desde la solapa Contactanos y lo resolvemos.' + codigo }; // 2026-09-18 - la solapa esta en esta misma pagina 807 } 808 return { ok: false, message: 'Tus datos se guardaron, pero no pudimos iniciar el pago. Volvé a intentarlo desde la página de suscripciones.' + codigo }; 809 810 } catch (error) { 811 console.error('[Checkout] Error desde el perfil:', error); 812 return { ok: false, message: 'Tus datos se guardaron, pero no pudimos iniciar el pago. Volvé a intentarlo desde la página de suscripciones. (código CK0)' }; 813 } 814} 815 816// Función para inicializar contador de caracteres en textarea 817function initTextareaCounter() { 818 const textarea = document.querySelector('[name="address_notes"]'); 819 if (!textarea) return; 820 821 const wrapper = textarea.closest('.textarea-wrapper'); 822 if (!wrapper) return; 823 824 const counter = wrapper.querySelector('.char-counter'); 825 const currentSpan = counter?.querySelector('.current'); 826 827 if (!counter || !currentSpan) return; 828 829 // Función para actualizar contador 830 function updateCounter() { 831 const length = textarea.value.length; 832 currentSpan.textContent = length; 833 834 // Agregar clase de advertencia cuando está cerca del lÃmite 835 if (length >= 180) { 836 counter.classList.add('warning'); 837 } else { 838 counter.classList.remove('warning'); 839 } 840 } 841 842 // Eventos 843 textarea.addEventListener('input', updateCounter); 844 845 // Inicializar con valor actual 846 updateCounter(); 847} 848 849// 2025-03-04 - Resaltar campos obligatorios vacÃos y hacer scroll al primero 850function highlightEmptyRequired(form) { 851 const requiredFields = [ 852 // 2026-09-18 - Se suma birth_date como obligatorio 853 'nombre', 'apellido', 'telefono', 'doc_type', 'doc_number', 'birth_date', 854 'address_street', 'address_number', 855 'country_code', 'province_id', 'city_text', 'postal_code' 856 ]; 857 858 let firstEmpty = null; 859 860 requiredFields.forEach(fieldName => { 861 const field = form.querySelector(`[name="${fieldName}"]`); 862 if (!field) return; 863 864 const value = field.value.trim(); 865 if (!value) { 866 // Marcar como inválido visualmente 867 field.classList.add('invalid'); 868 field.classList.remove('valid'); 869 870 // Para inputs con wrapper de validación 871 const wrapper = field.closest('.input-wrapper'); 872 const icon = wrapper ? wrapper.querySelector('.validation-icon') : null; 873 if (icon) { 874 icon.classList.add('invalid'); 875 icon.classList.remove('valid'); 876 } 877 878 if (!firstEmpty) firstEmpty = field; 879 } 880 }); 881 882 // Scroll al primer campo vacÃo 883 if (firstEmpty) { 884 firstEmpty.scrollIntoView({ behavior: 'smooth', block: 'center' }); 885 firstEmpty.focus(); 886 } 887} 888 889// Inicializar eventos para el formulario de información personal (actualizado) 890function initPersonalScreenEvents() { 891 const personalScreen = document.getElementById('personalScreen'); 892 if (!personalScreen) return; 893 894 const form = personalScreen.querySelector('form'); 895 if (!form) return; 896 897 // 2025-03-04 - Click en botón disabled: mostrar campos faltantes 898 // En lugar de disabled real, usamos una clase .is-disabled para poder capturar el click 899 const saveBtnEl = form.querySelector('.btn-save'); 900 if (saveBtnEl) { 901 saveBtnEl.addEventListener('click', function (e) { 902 if (this.classList.contains('is-disabled')) { 903 e.preventDefault(); 904 highlightEmptyRequired(form); 905 } 906 }); 907 } 908 909 // Track para saber qué campos han sido interactuados 910 const fieldInteracted = new Map(); 911 912 // Validación en tiempo real para todos los campos 913 const inputs = form.querySelectorAll('input, select, textarea');
914 inputs.forEach(input => { 915 // Para campos de texto, validar en input 916 input.addEventListener('input', (e) => { 917 // Marcar campo como interactuado 918 fieldInteracted.set(e.target.name, true); 919 920 if (e.target.type !== 'checkbox') { 921 const showValidation = fieldInteracted.get(e.target.name) || false; 922 validateField(e.target, showValidation); 923 } 924 validatePersonalFormLogic(); 925 }); 926 927 // Para selects y checkboxes, validar en change 928 input.addEventListener('change', (e) => { 929 // Marcar campo como interactuado 930 fieldInteracted.set(e.target.name, true); 931 932 if (e.target.type !== 'text' && e.target.type !== 'textarea') { 933 const showValidation = fieldInteracted.get(e.target.name) || false; 934 validateField(e.target, showValidation); 935 } 936 validatePersonalFormLogic(); 937 }); 938 }); 939 940 // Evento para cambio de paÃs 941 const countrySelect = form.querySelector('#country_code'); 942 if (countrySelect) { 943 countrySelect.addEventListener('change', (e) => { 944 const countryCode = e.target.value; 945 if (countryCode) { 946 loadProvinces(countryCode); 947 } else { 948 const provinceSelect = document.querySelector('#province_id'); 949 if (provinceSelect) { 950 provinceSelect.disabled = true; 951 provinceSelect.innerHTML = '<option value="">Seleccione provincia</option>'; 952 } 953 } 954 }); 955 } 956 957 // Evento para envÃo del formulario (actualizado para manejar checkbox) 958 form.addEventListener('submit', async (e) => { 959 e.preventDefault(); 960 961 const saveBtn = form.querySelector('.btn-save'); 962 if (!saveBtn || saveBtn.classList.contains('is-disabled')) return; 963 964 // Deshabilitar botón mientras procesa 965 saveBtn.disabled = true; 966 const originalText = saveBtn.textContent; 967 saveBtn.textContent = 'Guardando...'; 968 969 // Crear elemento para mensajes si no existe (usando misma clase que contraseña) 970 let messageElement = personalScreen.querySelector('.password-message'); 971 if (!messageElement) { 972 messageElement = document.createElement('div'); 973 messageElement.className = 'password-message'; 974 form.parentNode.insertBefore(messageElement, form.nextSibling); 975 } 976 977 function showMessage(message, type = 'info') { 978 messageElement.textContent = message; 979 messageElement.className = `password-message ${type}`; 980 messageElement.style.display = 'block'; 981 982 if (type === 'success') { 983 setTimeout(() => { 984 messageElement.style.display = 'none'; 985 showMainScreen(); 986 }, 2000); 987 } 988 } 989 990 try { 991 // Recolectar datos del formulario incluyendo checkbox 992 const formData = new FormData(form); 993 const dataObj = {}; 994 995 for (let [key, value] of formData.entries()) { 996 if (key === 'newsletter') { 997 // Para checkbox, enviar 1 o 0 998 dataObj[key] = 1; 999 } else { 1000 dataObj[key] = value.trim(); 1001 } 1002 } 1003 1004 // Si checkbox no está marcado, FormData no lo incluye, asà que agregarlo como 0 1005 if (!dataObj.hasOwnProperty('newsletter')) { 1006 dataObj.newsletter = 0; 1007 } 1008 1009 const result = await savePersonalData(dataObj); 1010 1011 if (result.ok) { 1012 // 2026-09-18 - Si vino del checkout con perfil incompleto, al guardar va directo al pago 1013 // showMessage(result.message, 'success'); 1014 const planPendiente = new URLSearchParams(window.location.search).get('plan'); 1015 if (planPendiente) { 1016 showMessage('Datos guardados. Te llevamos al pago...', 'info'); 1017 const checkout = await irAlPago(planPendiente); 1018 if (checkout.ok) { 1019 window.location.href = checkout.checkoutUrl; 1020 return; 1021 } 1022 showMessage(checkout.message, 'error'); 1023 } else { 1024 showMessage(result.message, 'success'); 1025 } 1026 } else { 1027 showMessage(result.message, 'error'); 1028 } 1029 1030 } catch (error) { 1031 console.error('Error en submit:', error); 1032 showMessage('Error inesperado. Inténtalo de nuevo.', 'error'); 1033 } finally { 1034 saveBtn.disabled = false; 1035 saveBtn.textContent = originalText; 1036 validatePersonalForm(); // Revalidar estado del botón 1037 } 1038 });
1039} 1040 1041// ========================= 1042// Funciones para cambio de contraseña (sin cambios) 1043// ========================= 1044 1045(function (window, document) { 1046 "use strict"; 1047 1048 function evaluarPassword(valor) { 1049 return { 1050 length: valor.length >= 8, 1051 number: /\d/.test(valor), 1052 uppercase: /[A-Z]/.test(valor), 1053 symbol: /[^\w\s]/.test(valor) // cualquier sÃmbolo no alfanumérico 1054 }; 1055 } 1056 1057 function todoOK(s) { 1058 return s.length && s.number && s.uppercase && s.symbol; 1059 } 1060 1061 function pintarRequisitos(root, s) { 1062 if (!root) return; 1063 const map = { 1064 length: root.querySelector('#req-length'), 1065 number: root.querySelector('#req-number'), 1066 uppercase: root.querySelector('#req-uppercase'), 1067 symbol: root.querySelector('#req-symbol') 1068 }; 1069 Object.entries(map).forEach(([clave, el]) => { 1070 if (!el) return; 1071 if (s[clave]) el.classList.add('met'); 1072 else el.classList.remove('met'); 1073 }); 1074 } 1075 // Evita re-configurar validación múltiples veces 1076 let _passwordInitDone = false; 1077 1078 window.initPasswordScreenValidation = function initPasswordScreenValidation() { 1079 const screen = document.getElementById('passwordScreen'); 1080 if (!screen) return; 1081 1082 const inputVieja = screen.querySelector('#oldPassword'); 1083 const inputNueva = screen.querySelector('#newPassword'); 1084 const btnGuardar = screen.querySelector('.btn-save'); 1085 const form = screen.querySelector('form'); 1086 1087 if (!inputNueva || !btnGuardar) return; 1088 1089 // Buscar o crear elemento para mensajes 1090 let messageElement = screen.querySelector('.password-message'); 1091 if (!messageElement) { 1092 messageElement = document.createElement('div'); 1093 messageElement.className = 'password-message'; 1094 // Insertar después del formulario o al inicio del screen 1095 if (form) { 1096 form.parentNode.insertBefore(messageElement, form.nextSibling); 1097 } else { 1098 screen.insertBefore(messageElement, screen.firstChild); 1099 } 1100 } 1101 1102 function showMessage(message, type = 'info') { 1103 messageElement.textContent = message; 1104 messageElement.className = `password-message ${type}`; 1105 messageElement.style.display = 'block'; 1106 1107 // Auto-ocultar mensajes de éxito después de 3 segundos 1108 if (type === 'success') { 1109 setTimeout(() => { 1110 messageElement.style.display = 'none'; 1111 messageElement.textContent = ''; 1112 }, 3000); 1113 } 1114 } 1115 1116 function hideMessage() { 1117 messageElement.style.display = 'none'; 1118 messageElement.textContent = ''; 1119 } 1120 1121 function refrescar() { 1122 const valor = inputNueva.value || ''; 1123 const state = evaluarPassword(valor); 1124 pintarRequisitos(screen, state); 1125 btnGuardar.disabled = !todoOK(state) || inputVieja.value.length <= 0; 1126 } 1127 1128 if (_passwordInitDone) { 1129 // si ya estaba inicializado, solo refrescamos estados y terminamos 1130 refrescar(); 1131 return; 1132 } 1133 1134 inputNueva.addEventListener('input', () => { 1135 hideMessage(); // Ocultar mensaje al escribir 1136 refrescar(); 1137 }); 1138 1139 inputVieja.addEventListener('input', () => { 1140 hideMessage(); // Ocultar mensaje al escribir 1141 refrescar(); 1142 }); 1143 1144 refrescar(); // estado inicial 1145 1146 if (form) { 1147 form.addEventListener('submit', async function (e) { 1148 e.preventDefault(); 1149 1150 if (btnGuardar.disabled) { 1151 return; 1152 } 1153 1154 // Deshabilitar botón mientras procesa 1155 btnGuardar.disabled = true; 1156 btnGuardar.textContent = 'Guardando...'; // Opcional: cambiar texto 1157 hideMessage(); 1158 1159 try { 1160 // AQUà está la corrección principal: await y manejo de respuesta 1161 const result = await firebaseApi.cambiarPassword(inputVieja.value, inputNueva.value); 1162 1163 if (result.ok) { 1164 // Ãxito 1165 showMessage(result.message || 'Contraseña actualizada correctamente.', 'success'); 1166 1167 // Limpiar campos 1168 inputVieja.value = ''; 1169 inputNueva.value = ''; 1170 refrescar(); 1171 1172 // Opcional: cerrar modal/pantalla después de éxito 1173 setTimeout(() => { 1174 showMainScreen(); 1175 }, 2000); 1176 1177 } else { 1178 // Error - AQUà se muestra el mensaje de contraseña incorrecta 1179 showMessage(result.message, 'error'); 1180 } 1181 1182 } catch (error) { 1183 console.error('Error inesperado en cambio de contraseña:', error); 1184 showMessage('Ocurrió un error inesperado. Por favor, intentá de nuevo.', 'error'); 1185 } finally { 1186 // Rehabilitar botón 1187 btnGuardar.disabled = false; 1188 btnGuardar.textContent = 'Guardar'; // Restaurar texto original 1189 refrescar(); // Refrescar estado del botón 1190 } 1191 }); 1192 } 1193 _passwordInitDone = true; 1194 }; 1195 1196 // ========================= 1197 // Inicialización principal 1198 // ========================= 1199 1200 function inicializarPerfil() { 1201 const perfil = document.querySelector('.profile-user'); 1202 if (!perfil) return; 1203 1204 // 2026-09-22 - El perfil viene oculto del HTML: hasta que Firebase no dice si hay sesion, 1205 // el que entra sin loguearse veia toda la pantalla de la cuenta antes de la redireccion. 1206 const solapas = document.querySelector('.profile-tabs'); 1207 const esperando = document.getElementById('profileEsperando'); 1208 1209 function mostrarPerfil() { 1210 perfil.hidden = false; 1211 if (solapas) solapas.hidden = false; 1212 if (esperando) esperando.remove(); 1213 } 1214 1215 // Red de seguridad: si Firebase no contesta nunca, el perfil se muestra igual. Que un 1216 // suscriptor se quede mirando una pagina en blanco es peor que el parpadeo que esto evita. 1217 const red = setTimeout(function () { 1218 console.warn('[Perfil] Firebase no contesto en 10s: se muestra el perfil igual'); 1219 mostrarPerfil(); 1220 }, 10000); 1221 1222 // Función que se ejecuta cuando Firebase está listo 1223 function onFirebaseReady() { 1224 clearTimeout(red); 1225 1226 if (window.firebaseApi && window.firebaseApi.isLoggedIn()) { 1227 mostrarPerfil(); 1228 perfil.querySelector('#current_email').innerHTML = firebase.auth().currentUser.email; 1229
1230 // Cargar datos del usuario (usa cache localStorage automáticamente) 1231 getUserProfileData(); 1232 1233 // Inicializar eventos del formulario personal 1234 initPersonalScreenEvents(); 1235 1236 // 2026-09-18 - Solapas 1237 initSolapas(); 1238 1239 // 2025-03-04 - Si viene del checkout con perfil incompleto, abrir directo 1240 if (new URLSearchParams(window.location.search).has('completar')) { 1241 showUpdateScreen('personal'); 1242 } 1243 1244 } else { 1245 console.log('Usuario no autenticado, redirigiendo...'); 1246 window.location.href = url_root; 1247 } 1248 } 1249 1250 // Verificar si Firebase ya está disponible 1251 if (window.firebaseApi) { 1252 onFirebaseReady(); 1253 } else { 1254 // Esperar a que Firebase se inicialice 1255 const checkFirebase = setInterval(() => { 1256 if (window.firebaseApi) { 1257 clearInterval(checkFirebase); 1258 onFirebaseReady(); 1259 } 1260 }, 100); 1261 } 1262 } 1263 1264 // Si la pantalla de password está activa al cargar, inicializa 1265 document.addEventListener('DOMContentLoaded', ()=> { 1266 const visiblePassword = document.querySelector('#passwordScreen.update-screen.active'); 1267 if (visiblePassword) { 1268 initPasswordScreenValidation(); 1269 } 1270 inicializarPerfil(); 1271 1272 }); 1273 1274})(window, document); 1275 1276// ========================= 1277// Funciones para paÃses y provincias 1278// ========================= 1279 1280// Función para obtener paÃses/provincias (con cache) 1281async function getCountriesData(forceRefresh = false) { 1282 const COUNTRIES_CACHE_KEY = 'countriesData'; 1283 const COUNTRIES_TIMESTAMP_KEY = 'countriesDataTimestamp'; 1284 const COUNTRIES_CACHE_DURATION = 60 * 60 * 1000; // 1 hora 1285 1286 // Intentar obtener del localStorage si no fuerza refresh 1287 if (!forceRefresh) { 1288 try { 1289 const cached = localStorage.getItem(COUNTRIES_CACHE_KEY); 1290 const timestamp = localStorage.getItem(COUNTRIES_TIMESTAMP_KEY); 1291 1292 if (cached && timestamp) { 1293 const age = Date.now() - parseInt(timestamp); 1294 if (age < COUNTRIES_CACHE_DURATION) { 1295 console.log('Usando datos de paÃses desde localStorage'); 1296 return JSON.parse(cached); 1297 } 1298 } 1299 } catch (error) { 1300 console.error('Error leyendo cache de paÃses:', error); 1301 } 1302 } 1303 1304 // Obtener del servidor 1305 try { 1306 console.log('Obteniendo datos de paÃses desde servidor...'); 1307 const response = await fetch(url_root + 'api/countries'); 1308 1309 if (!response.ok) { 1310 throw new Error(`HTTP ${response.status}`); 1311 } 1312 1313 const result = await response.json(); 1314 1315 if (result.ok && result.countries) { 1316 // Guardar en localStorage 1317 try { 1318 localStorage.setItem(COUNTRIES_CACHE_KEY, JSON.stringify(result.countries)); 1319 localStorage.setItem(COUNTRIES_TIMESTAMP_KEY, Date.now().toString()); 1320 console.log('Datos de paÃses guardados en localStorage'); 1321 } catch (error) { 1322 console.error('Error guardando cache de paÃses:', error); 1323 } 1324 1325 return result.countries; 1326 } else { 1327 throw new Error(result.error || 'Error obteniendo paÃses'); 1328 } 1329 1330 } catch (error) { 1331 console.error('Error obteniendo paÃses:', error); 1332 return null; 1333 } 1334} 1335 1336// Función para inicializar paÃses en el select 1337async function loadCountries() { 1338 const countrySelect = document.querySelector('#country_code'); 1339 if (!countrySelect) return; 1340 1341 try { 1342 const countriesData = await getCountriesData(); 1343 1344 if (!countriesData) { 1345 console.error('No se pudieron cargar los paÃses'); 1346 return; 1347 } 1348 1349 // Limpiar opciones existentes y mantener solo la opción por defecto 1350 countrySelect.innerHTML = '<option value="">Seleccione paÃs</option>'; 1351 1352 // Agregar paÃses
1353 Object.keys(countriesData).forEach(countryCode => { 1354 const country = countriesData[countryCode]; 1355 const option = document.createElement('option'); 1356 option.value = countryCode; 1357 option.textContent = country.name; 1358 1359 countrySelect.appendChild(option); 1360 }); 1361 1362 console.log('PaÃses cargados en el select'); 1363 1364 } catch (error) { 1365 console.error('Error cargando paÃses en select:', error); 1366 } 1367} 1368 1369// Función mejorada para cargar provincias 1370async function loadProvinces(countryCode, selectedProvinceId = null) { 1371 const provinceSelect = document.querySelector('#province_id'); 1372 if (!provinceSelect) return; 1373 1374 // Limpiar provincias actuales 1375 provinceSelect.innerHTML = '<option value="">Cargando provincias...</option>'; 1376 provinceSelect.disabled = true; 1377 1378 try { 1379 const countriesData = await getCountriesData(); 1380 1381 if (!countriesData || !countriesData[countryCode]) { 1382 provinceSelect.innerHTML = '<option value="">Error cargando provincias</option>'; 1383 return; 1384 } 1385 1386 const provinces = countriesData[countryCode].provinces; 1387 1388 // Reconstruir opciones 1389 provinceSelect.innerHTML = '<option value="">Seleccione provincia</option>'; 1390 1391 provinces.forEach(province => { 1392 const option = document.createElement('option'); 1393 option.value = province.id; 1394 option.textContent = province.name; 1395 1396 // Seleccionar si coincide con el valor actual 1397 if (selectedProvinceId && province.id == selectedProvinceId) { 1398 option.selected = true; 1399 } 1400 1401 provinceSelect.appendChild(option); 1402 }); 1403 1404 // Habilitar select 1405 provinceSelect.disabled = false; 1406 1407 console.log(`Cargadas ${provinces.length} provincias para ${countryCode}`); 1408 1409 } catch (error) { 1410 console.error('Error cargando provincias:', error); 1411 provinceSelect.innerHTML = '<option value="">Error cargando provincias</option>'; 1412 } 1413} 1414 1415// Función para limpiar también el cache de paÃses al logout 1416function clearAllCache() { 1417 clearUserProfileStorage(); 1418 localStorage.removeItem('countriesData'); 1419 localStorage.removeItem('countriesDataTimestamp'); 1420 userProfileData = null; 1421 console.log('Todo el cache limpiado'); 1422} 1423 1424// Exponer función de limpieza para el logout 1425window.clearUserSession = clearAllCache;
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.