PageSourceSearch

https://statics.forbes.com.ec/forbes/js/usuarios.profile.js?v=1790274916

js forbes.com.ec collected 2026-10-01 11:54:56 UTC 55,442 bytes, 1,425 lines download raw bytes

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.