PageSourceSearch

https://meteoro.noroeste.com.mx/public/renderer/profile.js?v=20251221

js noroeste.com.mx collected 2026-10-02 03:04:52 UTC 46,106 bytes, 1,305 lines download raw bytes

1window.renderForm = function(data, config) {
2
3	const target = document.querySelector(config.targetSelector);
4	if (!target) return;
5
6	const mensajePerfil = data.mensajePerfil || "";
7
8	const isLoggedIn = data.usuario && data.usuario.logueado;
9
10	const isSubscriber = data.usuario &&
11                     data.usuario.datos &&
12                     data.usuario.datos.suscriptor &&
13                     data.usuario.datos.suscriptor.suscripcion === true;
14
15	if (data.usuario) {
16		if (data.usuario.nuevo_token) {
17			setCrossDomainCookie('bx_token', data.usuario.nuevo_token);
18		}
19		if (data.usuario.logueado === false) {
20			setCrossDomainCookie('bx_token', "");
21		}
22	}
23
24    if (data.usuario.logueado == false) {
25        window.location.href = "/acceso";
26
27        return;
28    }
29
30    function inicialesDeNombre(nombre) {
31		if (!nombre) return '';
32		return nombre
33			.trim()
34			.split(/\s+/)
35			.slice(0, 2)
36			.map(p => p.charAt(0).toUpperCase())
37			.join('');
38	}
39    
40    function incrustarCodigo(html, target) {
41        // Resolver el contenedor destino
42        var el = target || document.body;
43        if (typeof target === 'string') {
44            el = document.querySelector(target) || document.body; // si no existe, cae en <body>
45        }
46
47        // Incrustar el HTML
48        var wrapper = document.createElement('div');
49        wrapper.innerHTML = html;
50        el.appendChild(wrapper);
51
52        // Ejecutar <script> embebidos (misma técnica que ya usas)
53        var scripts = wrapper.querySelectorAll('script');
54        scripts.forEach(function(oldScript) {
55            var newScript = document.createElement('script');
56
57            // Copiar atributos (src, type, etc.)
58            for (var i = 0; i < oldScript.attributes.length; i++) {
59                var attr = oldScript.attributes[i];
60                newScript.setAttribute(attr.name, attr.value);
61            }
62
63            // Copiar contenido inline
64            newScript.text = oldScript.text || oldScript.textContent || '';
65
66            // Reemplazar para disparar la ejecución
67            oldScript.parentNode.replaceChild(newScript, oldScript);
68        });
69    }
70
71    incrustarCodigo(mensajePerfil, target);
72    
73    function ponerDatos(data) {
74
75        usu = data.usuario.datos;
76
77        const ctx = {
78            nombre: usu.nombre || '',
79            correo: usu.correo || '',
80            iniciales: inicialesDeNombre(usu.nombre || '')
81        };
82
83        target.querySelectorAll('[data-n-texto]').forEach(function(el) {
84            const key = el.getAttribute('data-n-texto');
85            if (key && (key in ctx)) el.textContent = ctx[key] || '';
86        });
87
88        // Values
89        target.querySelectorAll('[data-n-valor]').forEach(function(el) {
90            const key = el.getAttribute('data-n-valor');
91            if (key && (key in ctx)) el.value = ctx[key] || '';
92        });
93
94    }
95
96    ponerDatos(data);
97
98    function ponerRedes(redes){
99	    // mapa por nombre de red
100        var map = {};
101        (redes || []).forEach(function(r){ map[r.red] = r; });
102
103        document.querySelectorAll('[data-n-social]').forEach(function(tile){
104            var red   = tile.getAttribute('data-n-social');      // p.ej. "google"
105            var info  = map[red] || null;
106
107            var status = tile.querySelector('[data-n-status]');
108            var badge  = tile.querySelector('[data-n-connected-badge]');
109            var btnCon = tile.querySelector('[data-n-connect]');
110            var btnDis = tile.querySelector('[data-n-disconnect]');
111
112            if (info) {
113                // conectado
114                if (status) status.textContent = info.correo ? ('Conectado como ' + info.correo) : 'Conectado';
115                if (badge)  badge.hidden  = false;
116                if (btnCon) btnCon.hidden = true;
117                if (btnDis) btnDis.hidden = false;
118                tile.setAttribute('data-n-state', 'connected');
119            } else {
120                // no conectado
121                if (status) status.textContent = 'No conectado';
122                if (badge)  badge.hidden  = true;
123                if (btnCon) btnCon.hidden = false;
124                if (btnDis) btnDis.hidden = true;
125                tile.setAttribute('data-n-state', 'disconnected');
126            }
127        });
128    }
129
130    ponerRedes(data.usuario.redes);
131
132    function formatearFechaEs(isoStr) {
133        if (!isoStr) return '';
134        const [y, m, d] = isoStr.split('-').map(Number);
135        const meses = ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'];
136        const dt = new Date(y, (m - 1), d);
137        if (isNaN(dt)) return '';
138        const dd = String(dt.getDate()).padStart(2, '0');
139        const mmm = meses[dt.getMonth()];
140        const yyyy = dt.getFullYear();
141
142        return `${dd}/${mmm}/${yyyy}`;
143    }
144
145    function renderTipo(tipo) {
146        const t = String(tipo || '').toLowerCase();
147        if (t === 'paypal') {
148            // Logo PayPal (puedes cambiar la URL luego por tu asset local)
149            const src = 'https://meteoro.noroeste.com.mx/public/imgs/paypal-svgrepo-com.svg';
150            return `<img src="${src}" alt="PayPal" style="height:58px; vertical-align:middle;">`;
151        }
152        if (t === 'openpay') {
153            const src = 'https://meteoro.noroeste.com.mx/public/imgs/credit-card.png';
154            return `<img src="${src}" alt="PayPal" style="height:42px; vertical-align:middle;">`;
155        }
156        return tipo || '';
157    }
158
159    function renderEstadoBadge(status) {
160        const s = String(status || '').toLowerCase();
161        let cls = 'text-bg-secondary';
162        if (s === 'activa') cls = 'text-bg-success';
163        else if (s === 'vencida') cls = 'text-bg-warning';
164        else if (s === 'cancelada') cls = 'text-bg-danger';
165        const label = s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
166        return `<span class="badge ${cls} badge-rounded">${label}</span>`;
167    }
168
169    function renderRenovacion(renovar) {
170        return (String(renovar).toUpperCase() === 'S') ? 'Automática' : 'No';
171    }
172
173    function renderAcciones(item) {
174        const t = String(item.tipo || '').toLowerCase();
175        const statusActiva = String(item.status || '').toLowerCase() === 'activa';
176        const renovarS = String(item.renovar || '').toUpperCase() === 'S';
177
178        const btns = [];
179
180        // Siempre incluimos data-id para que luego enlaces tus callbacks
181        const dataId = `data-n-id="${item.id}"`;
182
183        if (t === 'paypal' || t === 'openpay') {
184            // btns.push(`<a href="#" ${dataId} data-n-action="details" class="btn btn-outline-primary btn-sm text-nowrap">Ver detalles</a><br>`);
185
186            if (statusActiva && renovarS) {
187                btns.push(`<a href="#" ${dataId} data-n-action="cancel_renewal" class="btn btn-outline-danger btn-sm text-nowrap">Cancelar renovación</a>`);
188            } else if (statusActiva && !renovarS) {
189                btns.push(`<a href="#" ${dataId} data-n-action="cancel" class="btn btn-outline-danger btn-sm">Cancelar</a>`);
190            }
191        } else {
192            if (statusActiva) {
193                btns.push(`<a href="#" ${dataId} data-n-action="cancel" data-n-tipo="${t}" class="btn btn-outline-danger btn-sm">Cancelar</a>`);
194            }
195        }
196
197        // Si no hay botones, devolvemos un guion para no dejar la celda vacía
198        return btns.length ? btns.join(' ') : '&nbsp;';
199    }
200
201    function formatearFechaEsHM(dtStr) {
202        // dtStr: 'YYYY-MM-DD HH:mm:ss'
203        if (!dtStr) return '';
204        const [dPart, tPart='00:00:00'] = dtStr.split(' ');
205        const [y, m, d] = dPart.split('-').map(Number);
206        const [hh, mm, ss] = tPart.split(':').map(Number);
207        const meses = ['ene','feb','mar','abr','may','jun','jul','ago','sep','oct','nov','dic'];
208        const dd = String(d).padStart(2, '0');
209        const mmm = meses[(m-1) || 0];
210        const H  = String(hh||0).padStart(2,'0');
211        const M  = String(mm||0).padStart(2,'0');
212        return `${dd}/${mmm}/${y} ${H}:${M}`;
213    }
214    
215    function epochUTC(dtStr){
216        // para ordenar: convierte 'YYYY-MM-DD HH:mm:ss' a epoch ms (UTC)
217        if (!dtStr) return 0;
218        const [dPart, tPart='00:00:00'] = dtStr.split(' ');
219        const [y, m, d] = dPart.split('-').map(Number);
220        const [hh, mm, ss] = tPart.split(':').map(Number);
221        return Date.UTC(y, (m-1), d, hh||0, mm||0, ss||0);
222    }
223    
224    function formatMXN(v){
225        const n = Number(v);
226        if (isNaN(n)) return v ?? '';
227        const num = new Intl.NumberFormat('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n);
228        return `$${num} MXN`;
229    }
230    
231    function renderEstadoTx(status){
232        const s = String(status||'').toLowerCase();
233        const map = {
234            exitoso:     ['Pagado',       'text-bg-success'],
235            pendiente:   ['Pendiente',    'text-bg-warning'],
236            fallido:     ['Fallido',      'text-bg-secondary'], // puedes cambiar a 'text-bg-danger' si prefieres rojo
237            reembolsado: ['Reembolsado',  'text-bg-info']
238        };
239        const [txt, cls] = map[s] || [s, 'text-bg-secondary'];
240        return `<span class="badge ${cls} badge-rounded">${txt}</span>`;
241    }
242
243    document.addEventListener('change', function (ev) {
244        if (!ev.target.matches('#tab-newsletters input[type="checkbox"][data-n-news]')) return;
245
246        const ck = ev.target;                 
247        const id = ck.dataset.nNews;          
248        const activo = ck.checked;            
249
250        token = getCookie("bx_token");
251
252        formUtils.waitModal.show();
253        
254        fetch('https://meteoro.noroeste.com.mx/public/api/user/news', {
255        method: 'POST',
256            headers: {
257                'Content-Type': 'application/json'
258            },
259            body: JSON.stringify({ id: id, bx_siteId: config.siteId, token: token, activo: activo})
260        })
261        .then(r => r.json())
262        .then(function(data) { 
263            if (data.ok == true) {
264            
265                bxToast('Dato guardado correctamente', { type:'success', duration: 3000 });
266                
267            } else {
268
269                bxToast('Hubo un error al guardar el dato. Favor de intentar nuevamente mas tarde', { type:'danger', duration: 3000 });
270
271                if (data.error) {
272                    dlog("Error cURL:", data.error);
273                }
274                if (data.raw) {
275                    dlog("Respuesta sin JSON válido:", data.raw);
276                }
277
278            }
279        })
280        .catch(err => console.error('error:', err))
281        .finally(function(){
282
283            formUtils.waitModal.hide();
284
285        });
286    });
287
288    var susBtn = document.querySelector('button[data-n-action="suscribirme"]');
289
290    if (susBtn) {
291        susBtn.addEventListener('click', function (e) {
292            e.preventDefault();
293            e.stopPropagation();
294
295            window.location.href = "/suscripciones";
296        });
297    }
298
299    document.addEventListener("click", async function (ev) {
300        const b = ev.target.closest('button[data-n-metodo]');
301        if (b) {
302            ev.preventDefault();
303
304            cambiar_metodoPago(b);
305
306            return;
307        }
308
309        const c = ev.target.closest('button[data-n-metodo-eliminar]');
310        if (c) {
311            ev.preventDefault();
312
313            eliminar_metodoPago(c);
314
315            return;
316        }
317
318        const a = ev.target.closest('a[data-n-action]');
319        if (!a) return;
320
321        ev.preventDefault();
322
323        const action   = a.getAttribute("data-n-action"); // details | cancel_renewal | cancel
324        const id       = a.getAttribute("data-n-id") || "";
325        const tipo     = a.getAttribute("data-n-tipo") || "";
326
327        if (action == "cancel") {
328
329            tit = "Cancelar Suscripción";
330
331        } else if (action == "cancel_renewal") {
332            
333            tit = "Cancelar Renovación de Suscripción";
334
335        } else if (action == "details") { 
336            
337            // mostrar detalles
338            efectuarAccionSusc(action, id);
339
340            return;
341
342        } else if (action == "delete-account") { 
343
344            eliminarCuenta();
345
346            return;
347
348        } else {
349
350            return;
351
352        }
353
354        msg = "¿Seguro que deseas efectuar esta acción?";
355
356        if (action == "cancel" && tipo == "ios") {
357            msg = msg + "<br><br>Esta suscripción es de tipo iOS. La renovación automática se tiene que cancelar desde su cuenta de Apple";
358        }
359
360        formUtils.confirmModal.show({
361            title: tit,
362            message: msg,
363            onConfirm: function() {
364                
365                efectuarAccionSusc(action, id);
366
367            },
368            onCancel: function() {
369                
370                // console.log("El usuario canceló la acción");
371
372            }
373        });
374        
375    });
376
377    function eliminarCuenta() {
378
379        existeActiva = Array.from(
380            document.querySelectorAll('#tabla-suscripciones .badge')
381        ).some(badge => badge.textContent.trim().toLowerCase().includes('activa'));
382
383        existeIOSActiva = Array.from(
384            document.querySelectorAll('#tabla-suscripciones [data-n-tipo="ios"]')
385        ).some(btn => {
386            const estado = btn.closest('tr').querySelector('td:nth-child(5) .badge');
387            return estado && estado.textContent.trim().toLowerCase().includes("activa");
388        });
389
390        tit = "Eliminar cuenta";
391        msg = "¿Seguro que deseas efectuar esta acción? Esta operación no se puede deshacer.";
392
393        if (existeActiva) {
394            msg = msg + "<br><br><div style='font-size: 90%; color: #F00; background-color: #FF0; padding: 4px;'>Tiene una suscripción activa y será cancelada</div><br>";
395        }
396
397        if (existeIOSActiva) {
398            msg = msg + "<br><div style='font-size: 90%'>La renovación automática se tiene que cancelar desde su cuenta de Apple</div>";
399        }
400
401        formUtils.confirmModal.show({
402            title: tit,
403            message: msg,
404            onConfirm: function() {
405
406                token = getCookie("bx_token");
407                
408                formUtils.waitModal.show();
409
410                fetch('https://meteoro.noroeste.com.mx/public/api/user/delete', {
411                method: 'POST',
412                    headers: {
413                        'Content-Type': 'application/json'
414                    },
415                    body: JSON.stringify({ bx_siteId: config.siteId, token: token})
416                })
417                .then(r => r.json())
418                .then(function(data) { 
419                    if (data.res == "ok") {
420                    
421                        setCrossDomainCookie("bx_token", "");
422
423                        window.location.href = "https://cuenta.noroeste.com.mx/acceso";
424                        
425                    } else {
426                        
427                        formUtils.alertModal.show({
428                            title: "Error",
429                            message: "Hubo un error al intentar guardar el dato. " + data.error,
430                            type: "error"
431                        });
432
433                    }
434                })
435                .catch(err => console.error('error:', err))
436                .finally(function(){
437
438                    formUtils.waitModal.hide();
439
440                });
441
442            }
443        });
444
445    }
446
447    function efectuarAccionSusc(action, id) {
448
449        token = getCookie("bx_token");
450
451        formUtils.waitModal.show();
452        
453        fetch('https://meteoro.noroeste.com.mx/public/api/subscriptions/' + action, {
454        method: 'POST',
455            headers: {
456                'Content-Type': 'application/json'
457            },
458            body: JSON.stringify({ action: action, id: id, bx_siteId: config.siteId, token: token})
459        })
460        .then(r => r.json())
461        .then(function(data) { 
462            if (data.ok == true) {
463            
464                if (action == "cancel" || action == "cancel_renewal") {
465
466                    ponerSuscripciones(data.suscripciones);
467
468                } else if (action == "details") {
469
470
471
472                }
473                
474            } else {
475
476                formUtils.alertModal.show({
477                    title: "Error",
478                    message: "Hubo un error al intentar generar la acción",
479                    type: "error"
480                });
481
482            }
483        })
484        .catch(err => console.error('error:', err))
485        .finally(function(){
486
487            formUtils.waitModal.hide();
488
489        });
490
491    }
492
493    function ponerSuscripciones(suscripciones) {
494        const data = Array.isArray(suscripciones) ? suscripciones : [];
495
496        // ¿Paginamos? Solo si 8 o más
497        const enablePaging = data.length >= 8;
498
499        // Si ya existe una DataTable previa, destrúyela
500        if ($.fn.dataTable && $.fn.dataTable.isDataTable('#tabla-suscripciones')) {
501            $('#tabla-suscripciones').DataTable().clear().destroy();
502            $('#tabla-suscripciones tbody').empty();
503        }
504
505        // Mapear data a filas (incluyendo columna oculta para ordenar por id)
506        const rows = data.map(it => ([
507            it.nombre_publico || '',                 // Plan
508            formatearFechaEs(it.fec_ini),            // Inicio
509            formatearFechaEs(it.fec_fin),            // Vence
510            renderTipo(it.tipo),                     // Tipo
511            renderEstadoBadge(it.status),            // Estado
512            renderRenovacion(it.renovar),            // Renovación
513            renderAcciones(it),                      // Acciones
514            Number(it.id) || 0                       // _id_hidden (oculta, para ordenar desc)
515        ]));
516
517        $('#tabla-suscripciones').DataTable({
518            data: rows,
519            searching: false,        // sin buscador
520            lengthChange: false,     // sin selector "cuántos por página"
521            info: false,             // ocultar "Mostrando X de Y"
522            paging: enablePaging,    // solo si hay 8 o más
523            pageLength: 8,           // tamaño de página si hay paginación
524            ordering: true,
525            order: [[7, 'desc']],    // ordenar por _id_hidden (id) desc
526            language: {
527                emptyTable: "No hay suscripciones para mostrar. <a href='https://cuenta.noroeste.com.mx/suscripciones'>Ir pantalla de opciones</a>",
528                paginate: { previous: "Anterior", next: "Siguiente" }
529            }, 
530            autoWidth: false,
531            columnDefs: [
532                { targets: 0,  className:'text-nowrap' }, 
533                { targets: 1, width: '90px', className:'text-nowrap' }, // Inicio
534                { targets: 2, width: '90px', className:'text-nowrap' }, // Vence
535                { targets: 3, createdCell: function(td){ td.style.minWidth = '70px'; } }, 
536                { targets: 4, createdCell: function(td){ td.style.minWidth = '70px'; } }, 
537                { targets: 7, visible: false, searchable: false } 
538            ],
539        });
540    }
541
542    function ponerTransacciones(transacciones) {
543
544        const data = Array.isArray(transacciones) ? transacciones : [];
545        const enablePaging = data.length >= 8;
546
547        if ($.fn.dataTable && $.fn.dataTable.isDataTable('#tabla-transacciones')) {
548            $('#tabla-transacciones').DataTable().clear().destroy();
549            $('#tabla-transacciones tbody').empty();
550        }
551
552        const rows = data.map(it => ([
553            formatearFechaEsHM(it.fecha_movimiento), // Fecha (visible)
554            it.nombre_publico || '',                  // Concepto
555            formatMXN(it.monto),                      // Monto
556            renderEstadoTx(it.status),                // Estado
557            it.orden_id || '',                        // Referencia
558            epochUTC(it.fecha_movimiento)             // _fecha_hidden (para ordenar)
559        ]));
560
561        $('#tabla-transacciones').DataTable({
562            data: rows,
563            autoWidth: false,
564            searching: false,
565            lengthChange: false,
566            info: false,
567            paging: enablePaging,
568            pageLength: 8,
569            order: [[5, 'desc']], // ordenar por fecha oculta (más reciente primero)
570            columnDefs: [
571            { targets: 0, className:'text-nowrap' },             // Fecha
572            { targets: 2, className:'text-end text-nowrap' },    // Monto
573            { targets: 3, orderable:false },                     // Estado
574            { targets: 5, visible:false, searchable:false }      // _fecha_hidden
575            ],
576            language: {
577            emptyTable: "No hay transacciones para mostrar",
578            paginate: { previous: "Anterior", next: "Siguiente" }
579            }
580        });
581
582    }
583
584    function ponerMetodosPago_(metodos) {
585        var cont = document.getElementById('div-metodos');
586        if (!cont) return;
587        cont.innerHTML = 'Aún no tienes métodos de pago guardados. <br>Puedes agregarlos ahora para usarlos en compras o renovaciones automáticas.';
588
589        var PP_ICON = 'https://meteoro.noroeste.com.mx/public/imgs/paypal-svgrepo-com.svg';
590        var CC_ICON = 'https://meteoro.noroeste.com.mx/public/imgs/credit-card.png';
591
592        function two(v){ v=String(v||''); return v.length>2 ? v.slice(-2) : v.padStart(2,'0'); }
593        function brand(v){ return String(v||'').toUpperCase(); }
594        function vigencia(m,y){
595            m = parseInt(m,10)||0; y = String(y||'');
596            if (!m || !y || y==='0') return '';
597            return two(m) + '/' + two(y);
598        }
599
600        if (metodos.length > 0) {
601            cont.innerHTML = '';
602        }
603
604        metodos.forEach(function(m, idx){
605            var pas = String(m.pasarela||'').toLowerCase();
606            var isPaypal = pas === 'paypal';
607            var isPred   = String(m.es_predeterminado||'') === '1';
608
609            // contenedor de fila
610            var row = document.createElement('div');
611            row.className = 'd-flex align-items-center justify-content-between gap-3 border rounded-3 p-3 mb-2';
612            row.setAttribute('data-method-index', idx);
613            row.setAttribute('data-pasarela', pas);
614
615            // izquierda: icono + textos
616            var left = document.createElement('div');
617            left.className = 'd-flex align-items-center gap-3';
618
619            var iconWrap = document.createElement('div');
620            iconWrap.className = 'd-inline-flex align-items-center justify-content-center bg-light rounded-2';
621            iconWrap.style.width = '40px';
622            iconWrap.style.height = '40px';
623
624            var img = document.createElement('img');
625            img.src = isPaypal ? PP_ICON : CC_ICON;
626            img.alt = isPaypal ? 'PayPal' : 'Tarjeta';
627            img.style.width = '24px';
628            img.style.height = '24px';
629            img.style.objectFit = 'contain';
630            iconWrap.appendChild(img);
631
632            var textBox = document.createElement('div');
633
634            var title = document.createElement('div');
635            title.className = 'fw-semibold';
636
637            if (isPaypal) {
638                title.textContent = 'PayPal';
639            } else {
640                // Tarjeta: "Tarjeta terminación 5100"
641                var last4 = String(m.ultimos_4_digitos || '').replace(/\s+/g,'');
642                title.textContent = 'Tarjeta terminación ' + (last4 || '—');
643            }
644
645            var small = document.createElement('div');
646            small.className = 'text-muted small';
647
648            if (isPaypal) {
649                small.textContent = m.payer_email ? m.payer_email : '—';
650            } else {
651                var vig = vigencia(m.mes_expiracion, m.ano_expiracion);
652                var mk  = brand(m.marca_tarjeta);
653                small.textContent = (vig ? ('Vigencia ' + vig) : '') + (vig && mk ? ' — ' : '') + (mk || '');
654            }
655
656            textBox.appendChild(title);
657            textBox.appendChild(small);
658
659            left.appendChild(iconWrap);
660            left.appendChild(textBox);
661
662            // derecha: badge o botón
663            var right = document.createElement('div');
664            right.className = 'd-flex align-items-center gap-2';
665
666            if (isPred) {
667                var badge = document.createElement('span');
668                badge.className = 'badge rounded-pill text-bg-primary';
669                badge.textContent = 'Predeterminado';
670                right.appendChild(badge);
671            } else {
672                var btn = document.createElement('button');
673                btn.type = 'button';
674                btn.className = 'btn btn-outline-primary btn-sm';
675                btn.textContent = 'Hacer predeterminado';
676                // data-* útiles para que luego enlaces tu lógica
677                btn.setAttribute('data-n-metodo', m.id);
678                btn.setAttribute('data-method-index', idx);
679                btn.setAttribute('data-pasarela', pas);
680                if (isPaypal) btn.setAttribute('data-email', m.payer_email || '');
681                if (!isPaypal) btn.setAttribute('data-last4', m.ultimos_4_digitos || '');
682                right.appendChild(btn);
683            }
684
685            row.appendChild(left);
686            row.appendChild(right);
687            cont.appendChild(row);
688        });
689    }
690
691    function ponerMetodosPago(metodos) {
692        var cont = document.getElementById('div-metodos');
693        if (!cont) return;
694
695        cont.innerHTML = 'Aún no tienes métodos de pago guardados. <br>Puedes agregarlos ahora para usarlos en compras o renovaciones automáticas.';
696
697        var PP_ICON = 'https://meteoro.noroeste.com.mx/public/imgs/paypal-svgrepo-com.svg';
698        var CC_ICON = 'https://meteoro.noroeste.com.mx/public/imgs/credit-card.png';
699
700        function two(v){ v=String(v||''); return v.length>2 ? v.slice(-2) : v.padStart(2,'0'); }
701        function brand(v){ return String(v||'').toUpperCase(); }
702        function vigencia(m,y){
703            m = parseInt(m,10)||0; y = String(y||'');
704            if (!m || !y || y==='0') return '';
705            return two(m) + '/' + two(y);
706        }
707
708        if (metodos.length > 0) cont.innerHTML = '';
709
710        metodos.forEach(function(m, idx){
711            var pas = String(m.pasarela||'').toLowerCase();
712            var isPaypal = pas === 'paypal';
713            var isPred   = String(m.es_predeterminado||'') === '1';
714
715            // contenedor (mobile: columna, sm+: fila)
716            var row = document.createElement('div');
717            row.className = 'd-flex flex-column flex-sm-row align-items-start align-items-sm-center gap-3 border rounded-3 p-3 mb-2';
718            row.setAttribute('data-method-index', idx);
719            row.setAttribute('data-pasarela', pas);
720
721            // izquierda: icono + textos
722            var left = document.createElement('div');
723            left.className = 'd-flex align-items-center gap-3';
724
725            var iconWrap = document.createElement('div');
726            iconWrap.className = 'd-inline-flex align-items-center justify-content-center bg-light rounded-2';
727            iconWrap.style.width = '40px';
728            iconWrap.style.height = '40px';
729
730            var img = document.createElement('img');
731            img.src = isPaypal ? PP_ICON : CC_ICON;
732            img.alt = isPaypal ? 'PayPal' : 'Tarjeta';
733            img.style.width = '24px';
734            img.style.height = '24px';
735            img.style.objectFit = 'contain';
736            iconWrap.appendChild(img);
737
738            var textBox = document.createElement('div');
739
740            var title = document.createElement('div');
741            title.className = 'fw-semibold';
742
743            if (isPaypal) {
744                title.textContent = 'PayPal';
745            } else {
746                var last4 = String(m.ultimos_4_digitos || '').replace(/\s+/g,'');
747                title.textContent = 'Tarjeta terminación ' + (last4 || '—');
748            }
749
750            var small = document.createElement('div');
751            small.className = 'text-muted small';
752
753            if (isPaypal) {
754                small.textContent = m.payer_email ? m.payer_email : '—';
755            } else {
756                var vig = vigencia(m.mes_expiracion, m.ano_expiracion);
757                var mk  = brand(m.marca_tarjeta);
758                small.textContent = (vig ? ('Vigencia ' + vig) : '') + (vig && mk ? ' — ' : '') + (mk || '');
759            }
760
761            textBox.appendChild(title);
762            textBox.appendChild(small);
763
764            left.appendChild(iconWrap);
765            left.appendChild(textBox);
766
767            // derecha: acciones (mobile: abajo ocupando ancho, sm+: a la derecha)
768            var right = document.createElement('div');
769            right.className = 'ms-sm-auto w-100-';
770
771            var actions = document.createElement('div');
772            actions.className = 'd-flex flex-wrap gap-2 justify-content-start justify-content-sm-end';
773
774            if (isPred) {
775                var badge = document.createElement('span');
776                badge.className = 'badge rounded-pill text-bg-primary';
777                badge.textContent = 'Predeterminado';
778                actions.appendChild(badge);
779            } else {
780                var btn = document.createElement('button');
781                btn.type = 'button';
782                btn.className = 'btn btn-outline-primary btn-sm';
783                btn.textContent = 'Hacer predeterminado';
784                btn.setAttribute('data-n-metodo', m.id);
785                btn.setAttribute('data-method-index', idx);
786                btn.setAttribute('data-pasarela', pas);
787                if (isPaypal) btn.setAttribute('data-email', m.payer_email || '');
788                if (!isPaypal) btn.setAttribute('data-last4', m.ultimos_4_digitos || '');
789                actions.appendChild(btn);
790
791                var btnDel = document.createElement('button');
792                btnDel.type = 'button';
793                btnDel.className = 'btn btn-outline-danger btn-sm';
794                btnDel.textContent = 'Eliminar';
795                btnDel.setAttribute('data-n-metodo-eliminar', m.id);
796                actions.appendChild(btnDel);
797            }
798
799            right.appendChild(actions);
800
801            row.appendChild(left);
802            row.appendChild(right);
803            cont.appendChild(row);
804        });
805    }
806
807    function cambiar_metodoPago(btn) {
808
809        id = btn.getAttribute("data-n-metodo") || "";
810
811        if (id) {
812
813            token = getCookie("bx_token");
814
815            formUtils.waitModal.show();
816            
817            fetch('https://meteoro.noroeste.com.mx/public/api/subscriptions/change_pm', {
818            method: 'POST',
819                headers: {
820                    'Content-Type': 'application/json'
821                },
822                body: JSON.stringify({ id: id, bx_siteId: config.siteId, token: token})
823            })
824            .then(r => r.json())
825            .then(function(data) { 
826                if (data.ok == true) {
827                
828                    ponerMetodosPago(data.metodos_pago);
829                    
830                } else {
831                    
832                    formUtils.alertModal.show({
833                        title: "Error",
834                        message: "Hubo un error al intentar generar la acción",
835                        type: "error"
836                    });
837
838                }
839            })
840            .catch(err => console.error('error:', err))
841            .finally(function(){
842
843                formUtils.waitModal.hide();
844
845            });
846
847        }
848
849    }
850
851    idBtnEliminar = 0;
852
853    function eliminar_metodoPago(btn) {
854
855        id = btn.getAttribute("data-n-metodo-eliminar") || "";
856
857        if (id) {
858
859            idBtnEliminar = id;
860
861            formUtils.confirmModal.show({
862                title: "Eliminar método de pago",
863                message: "¿Seguro de eliminar este método de pago?",
864                onConfirm: function() {
865                    
866                    eliminar_metodoPago_2();
867
868                },
869                onCancel: function() {
870                    
871                    // console.log("El usuario canceló la acción");
872
873                }
874            });
875
876        }
877
878    }
879
880    function eliminar_metodoPago_2() {
881
882        token = getCookie("bx_token");
883
884        formUtils.waitModal.show();
885        
886        fetch('https://meteoro.noroeste.com.mx/public/api/subscriptions/delete_pm', {
887        method: 'POST',
888            headers: {
889                'Content-Type': 'application/json'
890            },
891            body: JSON.stringify({ id: idBtnEliminar, bx_siteId: config.siteId, token: token })
892        })
893        .then(r => r.json())
894        .then(function(data) { 
895            if (data.ok == true) {
896            
897                ponerMetodosPago(data.metodos_pago);
898                
899            } else {
900                
901                formUtils.alertModal.show({
902                    title: "Error",
903                    message: "Hubo un error al intentar generar la acción",
904                    type: "error"
905                });
906
907            }
908        })
909        .catch(err => console.error('error:', err))
910        .finally(function(){
911
912            formUtils.waitModal.hide();
913
914        });
915        
916    }
917
918    function accionRedes(btn) {
919
920        const tile = btn.closest('[data-n-social]');
921        const red  = tile && tile.getAttribute('data-n-social'); 
922
923        if (btn.matches('[data-n-connect]')) {
924            
925            iniciarLoginSocial(red, config.siteId, data.usuario.token_redes);
926
927        } else if (btn.matches('[data-n-disconnect]')) {
928            
929            accionRedesFetch(red, "disconnect", tile);
930
931        }
932
933    }
934
935    function accionRedesFetch(red, accion, tile) {
936
937        formUtils.waitModal.show({
938            message: "Espere un momento.."
939        });
940
941        token = getCookie("bx_token");
942
943        fetch('https://meteoro.noroeste.com.mx/public/api/user/social', {
944            method: 'POST',
945            headers: {
946                'Content-Type': 'application/json',
947                'Authorization': 'Bearer ' + token
948            },
949            body: JSON.stringify({ action: accion, network: red, bx_siteId: config.siteId })
950        })
951        .then(r => r.json())
952        .then(function(data) { 
953            if (data.res == "OK") {
954
955                formUtils.alertModal.show({
956                    title: "Red social",
957                    message: "Dato guardado correctamente.",
958                    type: "success"
959                });
960
961                if (accion == "disconnect") {
962                    setDisconnected(tile);
963                }
964                
965            } else {
966
967                formUtils.alertModal.show({
968                    title: "Error",
969                    message: "Hubo un error al intentar guardar el dato",
970                    type: "error"
971                });
972
973            }
974        })
975        .catch(err => console.error('error:', err))
976        .finally(function(){
977            formUtils.waitModal.hide();
978        });
979
980    }
981
982    function setDisconnected(tile){
983        const badge  = tile.querySelector('[data-n-connected-badge]');
984        const btnCon = tile.querySelector('[data-n-connect]');
985        const btnDis = tile.querySelector('[data-n-disconnect]');
986        const status = tile.querySelector('[data-n-status]');
987
988        if (badge)  badge.hidden  = true;
989        if (btnDis) btnDis.hidden = true;
990        if (btnCon) btnCon.hidden = false;
991        if (status) status.textContent = 'No conectado';
992    }
993
994    function setConnected(provider){
995        const badge  = tile.querySelector('[data-n-connected-badge]');
996        const btnCon = tile.querySelector('[data-n-connect]');
997        const btnDis = tile.querySelector('[data-n-disconnect]');
998        const status = tile.querySelector('[data-n-status]');
999
1000        if (badge)  badge.hidden  = true;
1001        if (btnDis) btnDis.hidden = true;
1002        if (btnCon) btnCon.hidden = false;
1003        if (status) status.textContent = 'No conectado';
1004    }
1005
1006    function activarRed(provider, correo){
1007        const tile   = document.querySelector('.tile[data-n-social="'+provider+'"]');
1008        if (!tile) return;
1009
1010        const badge  = tile.querySelector('[data-n-connected-badge]');
1011        const btnCon = tile.querySelector('[data-n-connect]');
1012        const btnDis = tile.querySelector('[data-n-disconnect]');
1013        const status = tile.querySelector('[data-n-status]');
1014
1015        if (badge)  badge.hidden  = false;                // mostrar badge "Conectado"
1016        if (btnCon) btnCon.hidden = true;                 // ocultar "Conectar"
1017        if (btnDis) btnDis.hidden = false;                // mostrar "Desconectar"
1018        if (status) status.textContent = correo
1019            ? ('Conectado como ' + correo)
1020            : 'Conectado';
1021
1022        tile.setAttribute('data-n-state', 'connected');   // (opcional) marca de estado
1023    }
1024
1025    function renderNewsletterChecks(datos) {
1026        // Normaliza claves a string y valores a boolean
1027        var state = {};
1028        for (var k in datos) if (datos.hasOwnProperty(k)) state[String(k)] = !!datos[k];
1029
1030        var nodes = document.querySelectorAll('#tab-newsletters input[type="checkbox"][data-n-news]');
1031        for (var i = 0; i < nodes.length; i++) {
1032            var el = nodes[i];
1033            var id = el.getAttribute('data-n-news'); // "1", "2", "3"
1034            var checked = !!state[id];               // default: false si no viene
1035            if (el.checked !== checked) el.checked = checked;
1036        }
1037    }
1038
1039    function leer_news() {
1040
1041        token = getCookie("bx_token");
1042
1043        fetch('https://meteoro.noroeste.com.mx/public/api/user/news_list', {
1044        method: 'POST',
1045            headers: {
1046                'Content-Type': 'application/json'
1047            },
1048            body: JSON.stringify({ bx_siteId: config.siteId, token: token})
1049        })
1050        .then(r => r.json())
1051        .then(function(data) { 
1052            if (data.ok == true) {
1053            
1054                renderNewsletterChecks(data.datos);
1055                
1056            } else {
1057
1058                dlog('Hubo un error al guardar el dato');
1059
1060                if (data.error) {
1061                    dlog("Error cURL:", data.error);
1062                }
1063                if (data.raw) {
1064                    dlog("Respuesta sin JSON válido:", data.raw);
1065                }
1066
1067            }
1068        })
1069        .catch(err => console.error('error:', err))
1070        .finally(function(){
1071
1072            formUtils.waitModal.hide();
1073
1074        });
1075
1076    }
1077
1078    const script = document.createElement("script");
1079    script.src = "https://meteoro.noroeste.com.mx/public/renderer/social_profile.js";
1080    script.onload = () => dlog("social_profile.js cargado");
1081    document.head.appendChild(script);
1082
1083    const script2 = document.createElement("script");
1084    script2.src = "https://kit.fontawesome.com/a331efe121.js";
1085    script2.onload = () => dlog("font awesome.js cargado");
1086    document.head.appendChild(script2);
1087
1088    document.addEventListener('click', function (e) {
1089        const btn1 = e.target.closest('[data-n-action="logout"]');
1090        if (btn1) { 
1091            cerrar_sesion(); 
1092            return; 
1093        }
1094
1095        const btn3  = e.target.closest('[data-n-connect],[data-n-disconnect]');
1096        if (btn3) { 
1097            e.preventDefault(); 
1098            accionRedes(btn3); 
1099            return; 
1100        }
1101
1102        const btn2 = e.target.closest('[data-n-action="password"]');
1103        if (btn2) { 
1104            cambiar_password();
1105            return;
1106        }
1107
1108        const btn4 = e.target.closest('[data-n-action="nombre"]');
1109        if (btn4) { 
1110            cambiar_nombre();
1111            return;
1112        }
1113
1114        const btn5 = e.target.closest('[data-n-action="add-payment-method"]');
1115        if (btn5) { 
1116            agregar_metodo();
1117            return;
1118        }
1119    });
1120
1121    function cambiar_password() {
1122
1123        const input = document.querySelector('[data-n-valor="password"]');
1124        const pwd = input ? input.value : '';
1125
1126        if (!pwd || pwd.length < 4) {
1127			alert('La nueva contraseña debe tener al menos 4 caracteres.');
1128
1129			if (input) { input.focus(); }
1130			
1131            return;
1132		}
1133
1134        token = getCookie("bx_token");
1135
1136        fetch('https://meteoro.noroeste.com.mx/public/api/user/change_password', {
1137            method: 'POST',
1138            headers: {
1139                'Content-Type': 'application/json',
1140                'Authorization': 'Bearer ' + token
1141            },
1142            body: JSON.stringify({ new_password: pwd, bx_siteId: config.siteId }), 
1143            credentials: 'include'
1144        })
1145        .then(r => r.json())
1146        .then(function(data) { 
1147            if (data.res == "OK") {
1148
1149                formUtils.alertModal.show({
1150                    title: "Correcto",
1151                    message: "Dato guardado correctamente.",
1152                    type: "success"
1153                });
1154                
1155            } else {
1156
1157                formUtils.alertModal.show({
1158                    title: "Error",
1159                    message: "Hubo un error al intentar guardar el dato",
1160                    type: "error"
1161                });
1162
1163            }
1164        })
1165        .catch(err => console.error('error:', err));
1166
1167    }
1168
1169    function cambiar_nombre() {
1170
1171        const input = document.querySelector('[data-n-valor="nombre"]');
1172        const nom = input ? input.value : '';
1173
1174        if (!nom || nom.length < 4) {
1175			alert('El nombre debe tener al menos 4 caracteres.');
1176
1177			if (input) { input.focus(); }
1178			
1179            return;
1180		}
1181
1182        token = getCookie("bx_token");
1183
1184        fetch('https://meteoro.noroeste.com.mx/public/api/user/change_name', {
1185            method: 'POST',
1186            headers: {
1187                'Content-Type': 'application/json',
1188                'Authorization': 'Bearer ' + token
1189            },
1190            body: JSON.stringify({ new_name: nom, bx_siteId: config.siteId }), 
1191            credentials: 'include'
1192        })
1193        .then(r => r.json())
1194        .then(function(data) { 
1195            if (data.res == "OK") {
1196
1197                formUtils.alertModal.show({
1198                    title: "Correcto",
1199                    message: "Dato guardado correctamente.",
1200                    type: "success"
1201                });
1202                
1203            } else {
1204                
1205                formUtils.alertModal.show({
1206                    title: "Error",
1207                    message: "Hubo un error al intentar guardar el dato",
1208                    type: "error"
1209                });
1210
1211            }
1212        })
1213        .catch(err => console.error('error:', err));
1214
1215    }
1216
1217    function cerrar_sesion() {
1218
1219        deleteCrossDomainCookie("bx_token");
1220
1221        formUtils.alertModal.show({
1222            title: "Sesión cerrada",
1223            message: "Has cerrado sesión correctamente.",
1224            type: "success", 
1225            onClose: function() {
1226                const loadingModal = window.formUtils.loadingModal; 
1227                loadingModal.show();
1228
1229                window.location.href = "/acceso";
1230            }
1231        });
1232        
1233    }
1234
1235    function agregar_metodo() {
1236
1237    }
1238
1239    window.onBxLoginSuccess = function(datos) {
1240        
1241        if (datos.res == "OK") {
1242
1243            activarRed(datos.provider, datos.correo);
1244
1245        } else {
1246
1247            formUtils.alertModal.show({
1248                title: "Red social",
1249                message: datos.msg,
1250                type: "error"
1251            });
1252
1253        }
1254
1255    };
1256
1257    (function() {
1258        // Crear script de jQuery
1259        var jq = document.createElement("script");
1260        jq.src = "https://code.jquery.com/jquery-3.7.1.min.js";
1261        jq.crossOrigin = "anonymous";
1262
1263        jq.onload = function() {
1264            dlog("jQuery cargado");
1265
1266            // Ahora sí cargamos DataTables
1267            var dt = document.createElement("script");
1268            dt.src = "https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js";
1269
1270            dt.onload = function() {
1271                dlog("dataTables cargado");
1272            };
1273
1274            document.head.appendChild(dt);
1275        };
1276
1277        document.head.appendChild(jq);
1278    })();
1279
1280    (function waitDT(){
1281
1282        if (window.jQuery && $.fn && $.fn.dataTable) {
1283            dlog("jQuery y dataTables cargados");
1284            
1285            ponerSuscripciones(data.usuario.suscripciones);
1286            ponerTransacciones(data.usuario.transacciones);
1287            ponerMetodosPago(data.usuario.metodos_pago);
1288			
1289			const tieneActiva = data.usuario.suscripciones.some(s => s.status === 'activa')
1290			
1291            if (tieneActiva) {
1292                var btn = document.querySelector('[data-n-action="suscribirme"]');
1293                btn.style.display = 'none';
1294            }
1295
1296            leer_news();
1297        } else {
1298            dlog("jQuery y dataTables NO cargados");
1299
1300            setTimeout(waitDT, 200);
1301        }
1302
1303    })();
1304	
1305};

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.