1 2<!DOCTYPE html> 3<html lang="en"> 4 5<head> 6 <!-- CustomHTMLHead (SiteJson) --><link rel="preconnect" href="https://fonts.googleapis.com"> 7<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 8<link href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap" rel="stylesheet"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 9 <meta http-equiv="Cache-Control" content="Public" /> 10 11 12 13 <title>HSR TERMAS - The best price, guaranteed!</title> 14 15 <link rel="canonical" href="https://www.sanremohoteles.com/en/list.html" /> 16 <link rel="alternate" hreflang="es-AR" href="https://www.sanremohoteles.com/list.html" /> 17 <link rel="alternate" hreflang="en-US" href="https://www.sanremohoteles.com/en/list.html" /> 18 <link rel="alternate" hreflang="pt-BR" href="https://www.sanremohoteles.com/pt/list.html" /> 19 <link rel="alternate" hreflang="fr-FR" href="https://www.sanremohoteles.com/fr/list.html" /> 20 <link rel="alternate" hreflang="x-default" href="https://www.sanremohoteles.com/list.html" /> 21 <meta property="og:url" content="https://www.sanremohoteles.com/en/list.html" /> 22 <meta property="og:type" content="website" /> 23 <meta property="og:title" content="HSR TERMAS - The best price, guaranteed!" /> 24 25 26 <meta name="description" content="Hotel HSR Termas se encuentra ubicado en pleno centro de la ciudad Termal de Rio Hondo, Santiago del Estero."> 27 <meta property="og:description" content="Hotel HSR Termas se encuentra ubicado en pleno centro de la ciudad Termal de Rio Hondo, Santiago del Estero." /> 28 <meta property="og:image" content="https://images-new.pxsol.com/lFRnkg-X1th2dYVtxIxo7d-DEunBB9BSFOoXoGNQVSw/rs:fill:630:430:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F23437%2Fcompany%2Flibrary%2Fuser%2F1602001633576143f09f9529628de298d9cc1c25e5f.jpg@jpg" /> 29 30 <link href="https://fonts.googleapis.com/css?family=Libre+Franklin:200,300,500&display=swap" rel="stylesheet"> 31 32 <link data-href="https://cdn.pxsol.com/css_3.css?pos=Sanremohoteles&device=computer&v=1790891789&posv=1788314973_2025-08-29T20:43:48.000Z&filev=1782498034" rel='stylesheet' type="text/css" /> 33 34 35 <link rel="icon" type="image/png" sizes="16x16" href="https://images-new.pxsol.com/i8aTnK9z_I7yMOE6uhTDRjt7u974dallnI0vpd-wgKc/rs:fill:16:16:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 36 <link rel="icon" type="image/png" sizes="32x32" href="https://images-new.pxsol.com/Na6WSYRu9xt0nvBwWoJXCjcZUU0hDjy94UBvLStJTnE/rs:fill:32:32:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 37 <link rel="icon" type="image/png" sizes="48x48" href="https://images-new.pxsol.com/vXQsDGVrobVKfmOIBuWkATPU3ay3i4mI-e7hG6GBTmQ/rs:fill:48:48:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 38 <link rel="icon" type="image/png" sizes="96x96" href="https://images-new.pxsol.com/qnK7mhDfUwm27MqRnhtiKMQtvFhqKxXMmPbKm2rPOkg/rs:fill:96:96:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 39 <link rel="shortcut icon" href="https://files-p.pxsol.com/13307/company/library/user/5126930364ea8a9a40b647af1c2cabae351ef5e772.png" /> 40 41 <link rel="apple-touch-icon" sizes="57x57" href="https://images-new.pxsol.com/0Ns7SAkuELF-9IkX4hwCBxt0JYnuWua_OiolB5A94JU/rs:fill:57:57:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 42 <link rel="apple-touch-icon" sizes="72x72" href="https://images-new.pxsol.com/Wf7bwULbfl4jw3AgLGjvEh_CbwL48bqth4VC5a_NJXk/rs:fill:72:72:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 43 <link rel="apple-touch-icon" sizes="76x76" href="https://images-new.pxsol.com/mUynYO5S4DHMv7SRfMIiemu5rchRgy8mvncyJ4HjxQ0/rs:fill:76:76:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 44 <link rel="apple-touch-icon" sizes="114x114" href="https://images-new.pxsol.com/tDzltV8zRUlgI2CqfSvTHm4CvOGkjw1lAtb188TIrqs/rs:fill:114:114:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 45 <link rel="apple-touch-icon" sizes="120x120" href="https://images-new.pxsol.com/DjfKoHijr5ulDWQ1sKqrOkBwJqiyAIKGu2cjFF-c7nw/rs:fill:120:120:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 46 <link rel="apple-touch-icon" sizes="144x144" href="https://images-new.pxsol.com/8Skc2RbdMtUnWpu_vtjiJvyDmIg1DBiaJ9VjO8kPlpo/rs:fill:144:144:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 47 <link rel="apple-touch-icon" sizes="152x152" href="https://images-new.pxsol.com/jOm0xThLFbk9O-i6qVU7-VOzF3PxvnB8Z3nUhCJdChU/rs:fill:152:152:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 48 <link rel="apple-touch-icon" sizes="180x180" href="https://images-new.pxsol.com/FHLyab3zm8ybaGF8mU38Z-T0jcfkqHDCWIp-bzF_RYU/rs:fill:180:180:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png"> 49 50 51 52 <link data-href="/neo_checkout/responsive.css?filev=1778144039" rel="stylesheet" type="text/css" /> 53 54 <link href="/neo_marketplace_lp/css.css?v=1790891789" rel="stylesheet" type="text/css" /> 55 56 <style> 57 .neo_bgcolor_primary, .MuiButton-containedSecondary { 58background-color: #b09d21 !important; 59} 60 61.MuiButton-containedSecondary.MuiButton-contained.Mui-disabled { 62 box-shadow: none !important; 63 background-color: rgba(0, 0, 0, 0.12) !important; 64} 65 66.neo_color_primary { 67color: #b09d21; 68} 69 70.cta_webcheckin_px:not(.btn_sesion) { 71background-color: #b09d21 !important; 72color: white !important; 73border: none !important;
74font-weight: bold; 75right: 0; 76display: flex; 77align-items: center; 78font-size: 13px !important; 79padding: 7px 8px !important; 80line-height: 1.2; 81text-align: left; 82} 83 84.cta_webcheckin_px.btn_sesion { 85border: none !important; 86font-weight: bold; 87right: 0; 88display: flex; 89align-items: center; 90font-size: 13px !important; 91padding: 7px 8px !important; 92line-height: 1.2; 93text-align: left; 94} 95 96.cta_webcheckin_px * { 97color: inherit !important; 98} 99 100.cta_webcheckin_px.btn_sesion > i { 101 color: #b09d21 !important; 102} 103 104.cta_webcheckin_px { 105background-color: #c0b14e; 106} 107.cta_webcheckin_px i { 108margin-right: 10px; 109font-size: 15px; 110} 111 112.neo_bgcolor_primary:hover, .MuiButton-containedSecondary:hover { 113background-color: #c0b14e; 114} 115.px-sm-inputcontainer:hover label { 116color: #b09d21 !important; 117} 118div.ui-datepicker#ui-datepicker-div .ui-state-active, div.ui-datepicker#ui-datepicker-div .ui-widget-content .ui-state-active, div.ui-datepicker#ui-datepicker-div .ui-widget-header .ui-state-active, div.ui-datepicker#ui-datepicker-div td:not(.ui-datepicker-today) .ui-state-highlight, div.ui-datepicker#ui-datepicker-div .ui-widget-content .ui-state-highlight, div.ui-datepicker#ui-datepicker-div .ui-widget-header .ui-state-highlight { 119background-color: #b09d21 !important; 120} 121 122.px-mk-iconchangesearch { 123color: #b09d21 !important; 124} 125 126.px-mk-locationdropdown>*:hover .px-mk-locationimg { 127fill: #b09d21 !important; 128} 129 130.neo_mobile_supratop .neo_supratop_selector i { 131color: #b09d21 !important; 132} 133 134.standard_cta { 135padding: 10px 15px; 136box-sizing: border-box; 137border-radius: 5px; 138display: flex; 139justify-content: center; 140text-align: center; 141align-items: center; 142background: #b09d21 !important; 143color: #000000; 144transition: .2s ease-out; 145color: white; 146text-decoration: none; 147font-size: 18px; 148border: none; 149transition: .25s ease-out; 150cursor: pointer; 151} 152 153.standard_cta:hover { 154background-color: #c8bb64; 155} 156.standard_cta:disabled { 157opacity: .5; 158cursor: not-allowed; 159} 160 161.neo_cta_naked {
162font-weight: bold; 163text-decoration: none; 164color: #b09d21; 165display: inline-flex; 166background-color: transparent; 167border: none; 168cursor: pointer; 169transition: .2s ease-out; 170} 171 172.neo_cta_naked.gray { 173color: darkgray; 174} 175 176.neo_cta_naked:hover { 177color: #7c6e18; 178} 179 180.neo_cta_standard { 181border: none; 182border-radius: 10px; 183padding: 12px 20px; 184font-size: 18px; 185display: flex; 186align-items: center; 187justify-content: space-between; 188font-weight: bold; 189cursor: pointer; 190transition: .25s ease-out; 191box-sizing: border-box; 192text-decoration: none; 193} 194 195.neo_cta_standard.smaller { 196font-size: 16px; 197padding: 7px 13px; 198} 199 200.neo_cta_standard.shadow { 201box-shadow: 0px -1px 15px rgba(0,0,0,.3); 202} 203 204.neo_cta_standard.lowpadding { 205padding: 12px; 206} 207.neo_cta_standard.lowpadding > i { 208margin: 0; 209} 210 211.neo_cta_standard:disabled { 212opacity: .5; 213cursor: not-allowed; 214} 215 216.neo_cta_standard.primary { 217background: #b09d21; 218color: #000000; 219} 220.neo_cta_standard.white_cta { 221background: white; 222color: black; 223} 224.neo_cta_standard.primary:hover { 225background-color: #7c6e18; 226color: #FFFFFF; 227} 228.neo_cta_standard.primary:active { 229background-color: #584f11; 230color: #FFFFFF; 231} 232 233.neo_cta_standard > i { 234margin-left: 10px; 235} 236 237.neo_cta_standard:disabled, .neo_cta_standard[disabled] {
238cursor: not-allowed; 239filter: saturate(.3); 240opacity: 0.8; 241} 242 243.neo_cta_standard.v_disabled { 244background-color: #1e1e1e; 245} 246 247.neo_cta_standard .loading-text, .neo_cta_standard .success-text { 248display:none; 249} 250 251.neo_cta_standard.v_loading { 252pointer-events: none; 253} 254.neo_cta_standard.v_loading { 255position: relative; 256} 257.neo_cta_standard .loading-text { 258display:none; 259} 260.neo_cta_standard.v_loading > *:not(i,.loading-text) { 261display:none; 262} 263.neo_cta_standard.v_loading > i { 264position: relative; 265height: 20px; 266width: 20px; 267} 268 269.neo_cta_standard.v_loading > i:before { 270content: none; 271} 272 273.neo_cta_standard.v_loading > i::after { 274content: ""; 275position: absolute; 276width: 100%; 277height: 100%; 278box-sizing: border-box; 279top: 0; 280left: 0; 281right: 0; 282bottom: 0; 283margin: auto; 284border: 0.25rem solid transparent; 285border-top-color: #ffffff; 286border-right-color: #ffffff; 287border-radius: 50%; 288animation: spinner .6s linear infinite; 289} 290.neo_cta_standard.v_loading .loading-text { 291display:block; 292} 293 294.neo_cta_standard.v_success { 295background-color: #00a651; 296} 297.neo_cta_standard.v_success > *:not(.success-text) { 298display:none; 299} 300.neo_cta_standard.v_success .success-text { 301display:block; 302} 303@keyframes button-loading-spinner { 304from { 305transform: rotate(0turn); 306} 307 308to { 309transform: rotate(1turn); 310} 311} 312 313@keyframes spinner { 314to { 315transform: rotate(360deg); 316} 317} 318 319#innerLoader, .innerLoader { 320height: 100%; 321display: flex; 322justify-content: center; 323align-items: center; 324min-height: 200px; 325} 326.neo_spin_loader { 327height: 60px; 328width: 60px; 329margin-bottom: 20px; 330} 331.neo_spin_loader:before { 332content: ""; 333box-sizing: border-box; 334position: absolute; 335width: 60px; 336height: 60px; 337border-radius: 50%; 338border: 3px solid #ccc; 339border-top-color: #b09d21; 340animation: spinner 1s linear infinite; 341} 342.neo_bgcolor_primary { 343background-color: #b09d21; 344} 345 346.neo_button_partyselector { 347border: 1px solid #b09d21; 348height: 40px; 349width: 40px; 350display: flex; 351justify-content: center; 352align-items: center; 353text-align: center; 354background: transparent; 355color: #b09d21; 356margin: 10px 0; 357transition: .2s ease-out; 358border-radius: 10px; 359font-size: 18px; 360cursor: pointer; 361 362} 363 364.neo_button_partyselector:hover { 365background-color: #e0d8a7; 366} 367 368.neo_button_partyselector:focus { 369box-shadow: 0 0 0 3px #f0ecd3; 370} 371 372.neo_button_partyselector:active { 373color: #000000; 374background: #b09d21; 375} 376 377.neo_partyselector_quantity { 378padding: 15px 10px; 379font-size: 18px; 380} 381 382.ChildrensAgeSelect { 383width: 100%; 384box-sizing: border-box; 385padding: 10px; 386margin-bottom: 10px; 387border-radius: 10px; 388border-color: #ccc; 389} 390 391.PartyTypeSimulator .px-mk-inputicon svg { 392height: 1.2em; 393width: 1.2em; 394fill: #aaa; 395} 396.neo_bgcolor_primary_text { 397color: #000000; 398} 399 400.neo_pxuser_mainbtn { 401height: 3.5rem; 402max-height: 100%; 403overflow: hidden; 404transition: .2s ease-out; 405border-radius: .5rem; 406cursor: pointer; 407padding: 0 0.5rem; 408background-color: white; 409color: black !important; 410} 411 412.neo_pxuser_mainbtn * { 413color: black !important; 414} 415 416.neo_pxuser_mainbtn:hover { 417background-color: #f2f2f2; 418} 419 420.neo_pxuser_avatar_container { 421height: 100%; 422width: 3rem; 423display: flex; 424justify-content: center; 425align-items: center; 426} 427 428.neo_pxuser_avatar_container img { 429height: 80%; 430border-radius: 2rem; 431} 432 433.neo_pxuser_text_block { 434padding: .5rem .5rem .5rem .5rem; 435box-sizing: border-box; 436height: 100%; 437justify-content: center; 438line-height: 1.5; 439} 440 441.neo_pxuser_icon_dd { 442padding: .5rem; 443box-sizing: border-box; 444} 445 446.neo_pxuser_dropdown { 447position: absolute; 448right: 0; 449top: 100% !important; 450background: white; 451border-radius: .25rem; 452min-width: 110%; 453box-shadow: 0 3px 6px rgba(0,0,0,.5); 454text-align: left; 455display: none !important; 456} 457 458.neo_pxuser_dropdown * { 459color: black !important; 460} 461 462.neo_pxuser_dropdown.open { 463display: flex !important; 464top: 100% !important; 465} 466 467.neo_pxuser_dd_button { 468padding: 1rem; 469transition: .2s ease-out; 470cursor: pointer; 471text-decoration: none; 472color: black; 473} 474 475.neo_pxuser_dd_button > .mdi { 476font-size: 1.2rem; 477line-height: 1; 478vertical-align: middle; 479display: inline-flex; 480width: 2rem; 481justify-content: center; 482align-items: center; 483height: 2rem; 484} 485 486.neo_pxuser_dd_button > span { 487padding: .5rem; 488display: inline-flex; 489flex-grow: 1; 490width: calc(100% - 3rem); 491} 492 493.neo_pxuser_dd_button:hover { 494background-color: #f2f2f2; 495border-radius: .25rem; 496} 497 498.neo_pxuser_email { 499 max-width: 8rem; 500 overflow: hidden; 501 text-overflow: ellipsis; 502} 503 504@media only screen and (max-width: 468px) { 505 506 .neo_pxuser_mainbtn { 507 flex-flow: row-reverse; 508 flex-grow: 1; 509 } 510 511 .neo_pxuser_icon_dd { 512 display: none; 513 } 514 515 .neo_pxuser_text_block { 516 text-align: right; 517 text-transform: none; 518 letter-spacing: initial; 519 color: black; 520 line-height: 1; 521 max-width: 50%; 522 } 523 524 .neo_pxuser_email { 525 display: none; 526 } 527 528 .neo_pxuser_container { 529 text-transform: initial; 530 letter-spacing: initial; 531 font-weight: normal; 532 font-size: .8rem; 533 } 534 535 .neo_pxuser_name { 536 font-weight: bold; 537 text-align: left; 538 white-space: nowrap; 539 overflow: hidden; 540 text-overflow: ellipsis; 541 display: flex; 542 align-items: center; 543 } 544 545 .neo_pxuser_name strong { 546 display: inline-block; 547 max-width: 150px; 548 overflow: hidden; 549 white-space: nowrap; 550 text-overflow: ellipsis; 551 vertical-align: middle; 552 line-height: 1.2; 553 } 554 555 .neo_pxuser_dd_button { 556 padding: .5rem; 557 } 558 559 560} </style> 561 562 <!-- Pos->Type: hotel.css --> 563 564<!-- Google tag (gtag.js) GA4 PXSOL TODOS LOS HOTELES -->
565<script> 566 window.dataLayer = window.dataLayer || []; 567 function gtag(){dataLayer.push(arguments);} 568 gtag('js', new Date()); 569</script>
vendor: 64 bytes, lines 569-570
569 570<script async src="https://www.googletagmanager.com/gtag/js?id=
570G-SYSQR6EL6E
vendor: 12 bytes, line 570
570"></script>
571<script> 572 let globalSessionGA4Config = { 573 'pos': 'Sanremohoteles', 574 'product_id': '24471', 575 'product_name': 'HSR TERMAS', 576 'product_country': 'Argentina', 577 'product_state': 'Santiago del Estero', 578 'product_plaza': 'Termas de RÃo Hondo', 579 'product_score': '92.38' 580 }; 581 582 gtag('config', 'G-2CG83MMWR8', globalSessionGA4Config); 583 584 gtag('config', 'G-SYSQR6EL6E', globalSessionGA4Config); 585 586 587 // Mirror page_view into dataLayer for GTM debug (guard to avoid duplicates) 588 if (!window.__dl_page_view_pushed) { 589 window.dataLayer.push({ event: 'page_view' }); 590 window.__dl_page_view_pushed = true; 591 } 592 593 594</script>
594 595 596<!-- Global site tag (gtag.js) -->
vendor: 64 bytes, lines 596-597
596 597<script async src="https://www.googletagmanager.com/gtag/js?id=
597UA-34517567-10
vendor: 12 bytes, line 597
597"></script>
598<script> 599 window.dataLayer = window.dataLayer || []; 600 601 function gtag() { 602 dataLayer.push(arguments); 603 try { 604 var args = Array.prototype.slice.call(arguments); 605 if (args[0] === 'event') { 606 var dlEventName = args[1]; 607 var dlParams = (args.length > 2 && typeof args[2] === 'object') ? args[2] : {}; 608 var dlObject = {}; 609 for (var key in dlParams) { 610 if (Object.prototype.hasOwnProperty.call(dlParams, key)) { 611 dlObject[key] = dlParams[key]; 612 } 613 } 614 dlObject.event = dlEventName; 615 window.dataLayer.push(dlObject); 616 617 // Intercept add_to_cart and fire form_success after it 618 if (dlEventName === 'add_to_cart') { 619 setTimeout(function() { 620 console.log('%c[pxTrack] ð FIRING form_success for add_to_cart', 'color: #4CAF50; font-weight: bold;'); 621 window.pxTrack('form_success', { 622 form_id: 'add_to_cart', 623 lead_id: null, 624 transaction_id: null 625 }); 626 }, 100); 627 } 628 } 629 } catch (e) {} 630 } 631 gtag('js', new Date()); 632 633 gtag('config', 'G-2CG83MMWR8'); 634 635 gtag('config', 'UA-34517567-10'); 636 637 638 </script>
638 639 640<!-- Pxsol Data Layer utilities and event wiring -->
641<script> 642 (function() { 643 // DEBUG: Confirm script is loaded and updated 644 console.log('%c[pxTrack] Script loaded - Version: 2025-01-15-no-inventory-v3-rate-limit-fix', 'color: #4CAF50; font-weight: bold; font-size: 12px;'); 645 646 // Define manual scan function globally FIRST (before any other code that might fail) 647 window.__px_manual_no_inventory_scan = function() { 648 console.log('[pxTrack] Manual scan triggered'); 649 if (window.__px_no_inventory_scan_func) { 650 return window.__px_no_inventory_scan_func(); 651 } else { 652 console.warn('[pxTrack] Watcher not initialized yet. Performing basic scan...'); 653 // Perform basic scan even if watcher not initialized 654 try { 655 var hasNoInventory = false; 656 var detectionMethod = ''; 657 658 // Check DOM elements 659 var noInventoryContainer = document.querySelector('#no-inventory-container'); 660 var noInventoryElement = document.querySelector('#no-inventory'); 661 if (noInventoryContainer || noInventoryElement) { 662 hasNoInventory = true; 663 detectionMethod = 'DOM element (ID)'; 664 console.log('[pxTrack] â Found #no-inventory-container or #no-inventory'); 665 } 666 667 // Check px_jsonQuery 668 if (!hasNoInventory && typeof window.px_jsonQuery !== 'undefined' && window.px_jsonQuery) { 669 var jsonQuery = window.px_jsonQuery; 670 if (jsonQuery.ProductList && Array.isArray(jsonQuery.ProductList) && jsonQuery.ProductList.length > 0) { 671 var allZero = true; 672 for (var i = 0; i < jsonQuery.ProductList.length; i++) { 673 var product = jsonQuery.ProductList[i]; 674 var productAvail = product.Availability || 0; 675 if (product.SkuList && Array.isArray(product.SkuList) && product.SkuList.length > 0) { 676 for (var j = 0; j < product.SkuList.length; j++) { 677 if (product.SkuList[j].Availability && product.SkuList[j].Availability > 0) { 678 allZero = false; 679 break; 680 } 681 } 682 } else if (productAvail > 0) { 683 allZero = false; 684 } 685 if (!allZero) break; 686 } 687 if (allZero) { 688 hasNoInventory = true; 689 detectionMethod = 'Json_Query'; 690 console.log('[pxTrack] â All products have Availability === 0'); 691 } 692 } 693 } 694 695 if (hasNoInventory && !window.__px_no_inventory_pushed) { 696 console.log('[pxTrack] ð FIRING no_inventory event manually - Method:', detectionMethod); 697 window.__px_no_inventory_pushed = true; 698 var params = {}; 699 try { 700 // Try to get search params 701 var startRaw = (document.querySelector('#fechaingreso') || {}).value || ''; 702 var endRaw = (document.querySelector('#fechaegreso') || {}).value || ''; 703 params.start_date = startRaw; 704 params.end_date = endRaw; 705 params.no_inventory = true; 706 } catch(e) {} 707 window.pxTrack('no_inventory', params); 708 709 // Fire form_success for search form after no_inventory 710 setTimeout(function() { 711 console.log('%c[pxTrack] ð FIRING form_success for search form (after manual no_inventory)', 'color: #4CAF50; font-weight: bold;'); 712 window.pxTrack('form_success', { 713 form_id: 'pxmk_searchform', 714 lead_id: null, 715 transaction_id: null 716 }); 717 }, 100); 718 719 return { fired: true, method: detectionMethod, params: params }; 720 } else if (hasNoInventory) { 721 return { already_fired: true, method: detectionMethod }; 722 } else { 723 return { no_inventory_detected: false, px_jsonQuery_exists: typeof window.px_jsonQuery !== 'undefined' }; 724 } 725 } catch(e) { 726 console.error('[pxTrack] Error in manual scan:', e); 727 return { error: 'Scan failed', message: e.message }; 728 } 729 } 730 }; 731 732 window.dataLayer = window.dataLayer || []; 733 734 // Helper that tracks to both GA4 and dataLayer 735 if (!window.pxTrack) { 736 window.pxTrack = function(eventName, params) { 737 params = params || {}; 738 try { gtag('event', eventName, params); } catch(e) {} 739 try { 740 var dl = {}; 741 for (var k in params) if (Object.prototype.hasOwnProperty.call(params, k)) dl[k] = params[k]; 742 dl.event = eventName; 743 window.dataLayer.push(dl); 744 } catch(e) {} 745 }; 746 } 747 748 // Privacy-safe booleans for PII presence 749 function bool(val) { return !!val; } 750 751 // Read URL param 752 function getUrlParam(name) {
753 try { 754 var params = new URLSearchParams(location.search); 755 return params.get(name) || ''; 756 } catch (e) { return ''; } 757 } 758 759 // Normalize common date formats to YYYY-MM-DD (best-effort) 760 function normalizeDate(value) { 761 try { 762 if (!value) return ''; 763 var v = String(value).trim(); 764 // dd/mm/yyyy -> yyyy-mm-dd 765 if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) { 766 var p = v.split('/'); 767 return [p[2], p[1].padStart(2, '0'), p[0].padStart(2, '0')].join('-'); 768 } 769 // Already ISO 770 if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return v; 771 return v; 772 } catch (e) { 773 return String(value || ''); 774 } 775 } 776 777 // Session id generator 778 function getPxSessionId() { 779 try { 780 var key = 'px_session_id'; 781 var match = document.cookie.match(new RegExp('(^| )' + key + '=([^;]+)')); 782 if (match) return match[2]; 783 var id = (crypto && crypto.randomUUID) ? crypto.randomUUID() : String(Date.now()) + '_' + Math.floor(Math.random()*1e9); 784 document.cookie = key + '=' + id + ';path=/;max-age=' + (60*60*24*365).toString(); 785 return id; 786 } catch(e) { 787 return 'sid_' + String(Date.now()); 788 } 789 } 790 791 // Expose chat helpers (to be called from chat widget) 792 window.pxChatOpen = function(payload) { window.pxTrack('chat_open', payload || {}); }; 793 window.pxChatMessage = function(payload) { window.pxTrack('chat_message', payload || {}); }; 794 window.pxChatClose = function(payload) { window.pxTrack('chat_close', payload || {}); }; 795 window.pxFormSuccess = function(payload) { window.pxTrack('form_success', payload || {}); }; 796 window.pxNoInventory = function(payload) { window.pxTrack('no_inventory', payload || { no_inventory: true }); }; 797 798 // Context payload from PHP (best-effort) 799 var pxContext = { 800 pos: 'Sanremohoteles', 801 product_id: '24471', 802 product_name: 'HSR TERMAS', 803 product_country: 'Argentina', 804 product_state: 'Santiago del Estero', 805 product_plaza: 'Termas de RÃo Hondo', 806 search_id: '', 807 start_date: '', 808 end_date: '', 809 transaction_id: '', 810 value: null, 811 currency: '', 812 page_url: location.href, 813 session_id: getPxSessionId() 814 }; 815 816 // --- External engine redirect auto event when engine is disabled --- 817 818 819 // Adults/children/rooms best-effort totals from PHP when available 820 pxContext.adults = 0; 821 pxContext.children = 0; 822 pxContext.rooms = 0; 823 824 // Push context for GTM usage 825 try { window.dataLayer.push(Object.assign({ event: 'px_context' }, pxContext)); } catch(e) {} 826 827 // Boot event para validar integración en DebugView/GTM 828 if (!window.__px_boot_pushed) { 829 window.__px_boot_pushed = true; 830 window.pxTrack('px_layer_ready', { 831 session_id: pxContext.session_id, 832 page_url: pxContext.page_url, 833 pos: pxContext.pos || '', 834 product_id: pxContext.product_id || '', 835 search_id: pxContext.search_id || '', 836 ts: Date.now() 837 }); 838 } 839 840 // ----- Search events wiring (V3/V8) ----- 841 function getSearchParamsFromDOM() { 842 var startRaw = (document.querySelector('.neo_marketplace_buscador_v3 #fechaingreso') || 843 document.querySelector('#fechaingreso') || 844 document.querySelector('[name="Start"]') || 845 document.querySelector('#Start') || { }).value || pxContext.start_date || ''; 846 var endRaw = (document.querySelector('.neo_marketplace_buscador_v3 #fechaegreso') || 847 document.querySelector('#fechaegreso') || 848 document.querySelector('[name="End"]') || 849 document.querySelector('#End') || { }).value || pxContext.end_date || ''; 850 var sidRaw = (document.querySelector('#SearchID') || { }).value || getUrlParam('SearchID') || pxContext.search_id || ''; 851 var params = { 852 start_date: normalizeDate(startRaw) || '', 853 end_date: normalizeDate(endRaw) || '', 854 pos: pxContext.pos || '', 855 hotel_id: pxContext.product_id || '', 856 page_url: location.href, 857 session_id: pxContext.session_id 858 };
859 if (sidRaw) params.search_id = sidRaw; 860 // Adults/children from UI if present 861 try { 862 var adultsEl = document.querySelector('.neo_partyselector_quantity[party-type="adults"]'); 863 var childrenEl = document.querySelector('.neo_partyselector_quantity[party-type="children"]'); 864 if (adultsEl) params.adults = Number(adultsEl.getAttribute('val') || adultsEl.textContent) || pxContext.adults || 0; 865 if (childrenEl) params.children = Number(childrenEl.getAttribute('val') || childrenEl.textContent) || pxContext.children || 0; 866 // Fallback from hidden GroupsForm if exists 867 if ((!params.adults || !params.children) && document.querySelector('#GroupsForm')) { 868 var gf = document.querySelector('#GroupsForm').value || ''; 869 var rooms = 0, adults = 0, children = 0; 870 if (gf) { 871 gf.split(';').forEach(function(groupStr){ 872 if (!groupStr) return; 873 rooms += 1; 874 var parts = groupStr.split(':'); 875 if (parts.length < 2) return; 876 var trio = parts[1].split(','); 877 var a = parseInt(trio[0] || '0', 10); 878 adults += isNaN(a) ? 0 : a; 879 var childrenAges = (trio[1] || '0'); 880 if (childrenAges && childrenAges !== '0') { 881 children += childrenAges.split('.').filter(Boolean).length; 882 } 883 }); 884 if (!params.adults) params.adults = adults; 885 if (!params.children) params.children = children; 886 if (!params.rooms) params.rooms = rooms; 887 } 888 } 889 } catch(e) {} 890 return params; 891 } 892 893 // -- Estado COMPARTIDO del throttle de `search_modify` -------------------------------- 894 // Vive FUERA de las funciones de cableado a proposito: `wireSearchEventsWhenReady()` puede 895 // re-ejecutar `wireSearchEvents()` varias veces hasta enganchar el buscador que llega por 896 // AJAX, y con el throttle dentro del cierre cada invocacion tenia su propia ventana de 1 s 897 // => dos cableados equivalentes podian emitir el mismo cambio dos veces. 898 var pxLastModifyMs = 0; 899 var pxLastSnapshot = null; 900 901 function getModifySnapshot() { 902 var p = getSearchParamsFromDOM(); 903 return { 904 start_date: p.start_date || '', 905 end_date: p.end_date || '', 906 rooms: (p.rooms*1) || 0, 907 adults: (p.adults*1) || 0, 908 children: (p.children*1) || 0 909 }; 910 } 911 function pushModifyFlagsIfChanged(prevSnap, nextSnap) { 912 try { 913 if (!prevSnap) return true; 914 var changes = {}; 915 if (nextSnap.start_date !== prevSnap.start_date) changes.start_date = true; 916 if (nextSnap.end_date !== prevSnap.end_date) changes.end_date = true; 917 if ((nextSnap.rooms*1) !== (prevSnap.rooms*1)) changes.rooms = true; 918 if ((nextSnap.adults*1) !== (prevSnap.adults*1)) changes.adults = true; 919 if ((nextSnap.children*1) !== (prevSnap.children*1)) changes.children = true; 920 if (Object.keys(changes).length) return true; 921 } catch (e) {} 922 return false; 923 } 924 function throttledModifyFire() { 925 var now = Date.now(); 926 if (now - pxLastModifyMs < 1000) return; // throttle to 1s 927 pxLastModifyMs = now; 928 var current = getModifySnapshot(); 929 // Push GA/DataLayer event with current values 930 window.pxTrack('search_modify', getSearchParamsFromDOM()); 931 // Also push a probe event with the current values for GTM debug 932 try { 933 window.dataLayer.push({ 934 event: 'px_search_modify_probe', 935 search_modify: current, 936 ts: Date.now() 937 }); 938 } catch(e) {} 939 // Update snapshot only if something changed 940 if (pushModifyFlagsIfChanged(pxLastSnapshot, current)) { 941 pxLastSnapshot = current; 942 } 943 } 944 945 /** 946 * Respaldos de `search_modify` que NO dependen de que el buscador exista en el DOM: 947 * listeners DELEGADOS en `document` (captura) + un poller de 400 ms sobre 948 * `#fechaingreso`/`#fechaegreso`/`#GroupsForm`. 949 * 950 * OJO: se instalan SIEMPRE en el init, sin pasar por `searchDomPresent()` ni por el tope de 951 * 20 s del watcher -- que es exactamente como se instalaban antes, cuando `wireSearchEvents()` 952 * se llamaba incondicionalmente. Justamente por ser delegados/poll son los que hacen que 953 * `search_modify` funcione sobre un buscador inyectado por AJAX: meterlos detras del 954 * reintento dejaba sin ningun listener a las sesiones donde el buscador tarda mas de 20 s 955 * (telefono con red lenta), que ANTES si los tenian. 956 */ 957 function wireSearchFallbacks() { 958 if (window.__px_search_fallbacks_wired) return; 959 window.__px_search_fallbacks_wired = true; 960 pxLastSnapshot = getModifySnapshot(); 961 // Debug: signal that search watchers are alive
962 try { 963 window.dataLayer.push({ 964 event: 'px_search_watchers_ready', 965 ts: Date.now() 966 }); 967 } catch(e) {} 968 // Listeners delegados (capturan aunque el DOM se re-renderice) 969 try { 970 if (!window.__px_global_search_listeners) { 971 window.__px_global_search_listeners = true; 972 var selectorList = ['#fechaingreso','#fechaegreso','.AdultsSelect','.ChildrensSelect','.neo_partyselector_quantity','.ChildrensAgeSelect']; 973 function matchesTracked(el) { 974 try { return selectorList.some(function(sel){ return el && el.matches && el.matches(sel); }); } catch(e) { return false; } 975 } 976 document.addEventListener('change', function(e){ if (matchesTracked(e.target)) throttledModifyFire(); }, true); 977 document.addEventListener('input', function(e){ if (matchesTracked(e.target)) throttledModifyFire(); }, true); 978 document.addEventListener('click', function(e){ 979 var t = e.target; 980 if (t && t.classList && t.classList.contains('neo_button_partyselector')) throttledModifyFire(); 981 }, true); 982 } 983 } catch(e) {} 984 // Robust watchers for datepicker-driven value changes and hidden GroupsForm updates 985 try { 986 if (!window.__px_modify_watchers_started) { 987 window.__px_modify_watchers_started = true; 988 var lastIn = (document.querySelector('#fechaingreso') || {}).value || ''; 989 var lastOut = (document.querySelector('#fechaegreso') || {}).value || ''; 990 var lastGF = (document.querySelector('#GroupsForm') || {}).value || ''; 991 setInterval(function(){ 992 try { 993 var inEl = document.querySelector('#fechaingreso'); 994 var outEl = document.querySelector('#fechaegreso'); 995 var gfEl = document.querySelector('#GroupsForm'); 996 var curIn = inEl ? inEl.value : ''; 997 var curOut = outEl ? outEl.value : ''; 998 var curGF = gfEl ? gfEl.value : ''; 999 if (curIn !== lastIn || curOut !== lastOut || curGF !== lastGF) { 1000 lastIn = curIn; lastOut = curOut; lastGF = curGF; 1001 throttledModifyFire(); 1002 } 1003 } catch(e) {} 1004 }, 400); 1005 } 1006 } catch(e) {} 1007 } 1008 1009 /** 1010 * SOLO los cableados que necesitan el nodo del buscador en el DOM: `search_open` (existe el 1011 * contenedor) y `search_submit` (existe el form). Los reintenta `wireSearchEventsWhenReady()`. 1012 * 1013 * OJO: ya NO cuelga listeners de `search_modify` del contenedor. Eran un duplicado EXACTO de 1014 * los delegados de `wireSearchFallbacks()` (la misma lista de 6 selectores y el mismo click 1015 * sobre `.neo_button_partyselector`). Mientras esta funcion corria una sola vez compartian el 1016 * cierre -- y con el el throttle -- asi que no se notaba; con el reintento podian quedar en 1017 * invocaciones distintas y contar el mismo cambio dos veces. 1018 */ 1019 function wireSearchEvents() { 1020 var searchForm = document.querySelector('.neo_marketplace_buscador_v3 #pxmk_searchform') || document.querySelector('#search_form'); 1021 var searchContainer = document.querySelector('.neo_marketplace_buscador_v3') || document.querySelector('.px-mk-buscador-container') || document.querySelector('#search_form') || document.querySelector('.PartyTypeSimulatorContainer'); 1022 1023 if (searchContainer && !window.__px_search_container_bound) { 1024 window.__px_search_container_bound = true; 1025 // fire search_open once 1026 if (!window.__px_search_open_pushed) { 1027 window.__px_search_open_pushed = true; 1028 window.pxTrack('search_open', { page_url: location.href }); 1029 } 1030 } 1031 1032 if (searchForm && !window.__px_search_form_bound) { 1033 window.__px_search_form_bound = true; 1034 searchForm.addEventListener('submit', function() { 1035 window.pxTrack('search_submit', getSearchParamsFromDOM()); 1036 }, true); 1037 var searchBtn = document.querySelector('.neo_marketplace_buscador_v3 #PxMkAcion_Search') || document.querySelector('#Acion_Search'); 1038 if (searchBtn) { 1039 searchBtn.addEventListener('click', function() {
1040 window.pxTrack('search_submit', getSearchParamsFromDOM()); 1041 }, true); 1042 } 1043 } 1044 } 1045 1046 /** 1047 * El buscador NO existe al DOMContentLoaded en NINGUNA `lp.html`: lo inyecta por AJAX 1048 * `neo_modules/lp_cart.php` (CargarModulo) cientos de ms despues. `wireSearchEvents()` se 1049 * cableaba UNA sola vez, asi que sus dos gates de DOM evaluaban null y no se emitian nunca 1050 * `search_open` (gate `if (searchContainer)`) ni `search_submit` (gate `if (searchForm)`), 1051 * ni `start_search`/`egreso_search` (`wireDatePickers`). Sólo disparaban en las HOME 1052 * server-rendered, donde el buscador viene en el HTML inicial. 1053 * 1054 * Medido en vivo el 2026-08-21 sobre `fincaogawa.reservadirecto.com/lp.html`: los 6 1055 * selectores del buscador estan en 0 en DOMContentLoaded (1389 ms) y en load (2369 ms), y 1056 * recien aparecen a los ~3010 ms; un click en el submit real (`#Acion_Search`) generaba 1057 * SearchID y `has_inventory`, pero CERO `search_submit`. 1058 * 1059 * `has_inventory`/`no_inventory` nunca sufrieron esto porque cuelgan de una cascada de scans 1060 * + MutationObserver (`wireNoInventoryWatcher` / `observeAvailabilityContainers`), no de un 1061 * cableado one-shot. Esta funcion aplica esa MISMA tecnica a la fam
1061ilia de busqueda. 1062 * 1063 * Se apaga sola en cuanto engancha el contenedor y el form (los dos productores de 1064 * `search_open`/`search_submit`), con tope duro de 20 s. Cada bind es idempotente por su 1065 * propia guarda global, asi que re-correr no duplica ni un listener ni un evento. 1066 */ 1067 function searchDomPresent() { 1068 try { 1069 return !!(document.querySelector('.neo_marketplace_buscador_v3') 1070 || document.querySelector('.px-mk-buscador-container') 1071 || document.querySelector('#search_form') 1072 || document.querySelector('.PartyTypeSimulatorContainer') 1073 || document.querySelector('#fechaingreso')); 1074 } catch (e) { return false; } 1075 } 1076 function wireSearchEventsWhenReady() { 1077 if (window.__px_search_ready_watcher) return; 1078 window.__px_search_ready_watcher = true; 1079 var mo = null; 1080 var stopped = false; 1081 function stop() { 1082 if (stopped) return; 1083 stopped = true; 1084 if (mo) { try { mo.disconnect(); } catch (e) {} mo = null; } 1085 } 1086 function attempt() { 1087 if (stopped) return; 1088 // Chequeo barato primero: sin buscador en el DOM no se toca nada. Evita correr 1089 // `wireSearchEvents()` (que lee bastante DOM) en cada mutacion de una pagina sin motor. 1090 if (!searchDomPresent()) return; 1091 if (!window.__px_search_container_bound || !window.__px_search_form_bound) { 1092 try { wireSearchEvents(); } catch (e) {} 1093 } 1094 if (!window.__px_date_pickers_bound) { 1095 try { wireDatePickers(); } catch (e) {} 1096 } 1097 if (window.__px_search_container_bound && window.__px_search_form_bound) stop(); 1098 } 1099 attempt(); 1100 if (stopped) return; 1101 [200, 500, 1000, 2000, 3000, 5000, 8000, 12000].forEach(function(ms){ 1102 setTimeout(attempt, ms); 1103 }); 1104 setTimeout(stop, 20000); 1105 if ('MutationObserver' in window && document.body) { 1106 try { 1107 mo = new MutationObserver(function(){ 1108 if (stopped || mo.__px_scheduled) return; 1109 mo.__px_scheduled = true; 1110 setTimeout(function(){ if (mo) { mo.__px_scheduled = false; } attempt(); }, 100); 1111 }); 1112 mo.observe(document.body, { childList: true, subtree: true }); 1113 } catch (e) { mo = null; } 1114 } 1115 } 1116 1117 // ----- Form events wiring (generic) ----- 1118 function wireFormEvents() { 1119 var forms = Array.prototype.slice.call(document.querySelectorAll('form:not([data-px-ignore="true"])')); 1120 var observed = new WeakSet(); 1121 1122 // form_view when enters viewport first time 1123 if ('IntersectionObserver' in window) { 1124 var io = new IntersectionObserver(function(entries) { 1125 entries.forEach(function(entry) { 1126 if (entry.isIntersecting && !observed.has(entry.target)) { 1127 observed.add(entry.target); 1128 window.pxTrack('form_view', { 1129 form_id: entry.target.id || entry.target.getAttribute('name') || 'form', 1130 page_url: location.href 1131 }); 1132 } 1133 }); 1134 }, { threshold: 0.2 }); 1135 forms.forEach(function(f){ io.observe(f); }); 1136 } else { 1137 forms.forEach(function(f){ 1138 window.pxTrack('form_view', { form_id: f.id || f.getAttribute('name') || 'form', page_url: location.href }); 1139 }); 1140 } 1141 1142 // form_interaction on field focus 1143 document.addEventListener('focusin', function(e) { 1144 var field = e.target; 1145 var form = field.form; 1146 if (!form) return; 1147 var fieldName = field.name || field.id || field.type || 'field'; 1148 try { 1149 if (field.classList && field.classList.contains('neo_partyselector_quantity')) { 1150 var pt = field.getAttribute('party-type'); 1151 if (pt) fieldName = 'party_' + pt; 1152 } 1153 } catch(e) {} 1154 window.pxTrack('form_interaction', { 1155 form_id: form.id || form.getAttribute('name') || 'form', 1156 field_name: fieldName 1157 }); 1158 }); 1159 1160 // form_submit with privacy-safe flags 1161 forms.forEach(function(form) { 1162 form.addEventListener('submit', function() { 1163 var email = form.querySelector('input[type="email"]'); 1164 var phone = form.querySelector('input[type="tel"], input[name*="phone"], input[name*="telefono"]'); 1165 var required = Array.prototype.slice.call(form.querySelectorAll('[required]')); 1166 var incomplete = required.some(function(el){ return !el.value; }); 1167 window.pxTrack('form_submit', { 1168 form_id: form.id || form.getAttribute('name') || 'form', 1169 page_url: location.href, 1170 fields_status: incomplete ? 'incompletos' : 'completos', 1171 email_provided: bool(email && email.value), 1172 phone_provided: bool(phone && phone.value), 1173 session_id: pxContext.session_id 1174 }); 1175 }, true); 1176 }); 1177 } 1178 // Extra: map clicks/changes on party selector controls to meaningful field_name 1179 (function wirePartySelectorInteractions(){
1180 try { 1181 var lastMs = 0; 1182 function pushPartyInteraction(partyType) { 1183 var now = Date.now(); 1184 if (now - lastMs < 400) return; 1185 lastMs = now; 1186 var form = document.querySelector('#search_form') || document.querySelector('.neo_marketplace_buscador_v3 #pxmk_searchform'); 1187 window.pxTrack('form_interaction', { 1188 form_id: form ? (form.id || form.getAttribute('name') || 'form') : 'form', 1189 field_name: partyType ? ('party_' + partyType) : 'party_selector' 1190 }); 1191 } 1192 document.addEventListener('click', function(e){ 1193 var t = e.target; 1194 if (t && t.classList && t.classList.contains('neo_button_partyselector')) { 1195 pushPartyInteraction(t.getAttribute('party-type') || ''); 1196 } 1197 }, true); 1198 document.addEventListener('change', function(e){ 1199 var t = e.target; 1200 if (t && t.classList && t.classList.contains('neo_partyselector_quantity')) { 1201 pushPartyInteraction(t.getAttribute('party-type') || ''); 1202 } 1203 }, true); 1204 document.addEventListener('input', function(e){ 1205 var t = e.target; 1206 if (t && t.classList && t.classList.contains('neo_partyselector_quantity')) { 1207 pushPartyInteraction(t.getAttribute('party-type') || ''); 1208 } 1209 }, true); 1210 } catch(e) {} 1211 })(); 1212 1213 // Fire specific date interactions (optional) 1214 function wireDatePickers() { 1215 if (window.__px_date_pickers_bound) return; 1216 var inEl = document.querySelector('.neo_marketplace_buscador_v3 #fechaingreso'); 1217 var outEl = document.querySelector('.neo_marketplace_buscador_v3 #fechaegreso'); 1218 // Todavia no llego el buscador: NO se marca como cableado, lo reintenta 1219 // `wireSearchEventsWhenReady()`. 1220 if (!inEl && !outEl) return; 1221 window.__px_date_pickers_bound = true; 1222 if (inEl) inEl.addEventListener('change', function(){ window.pxTrack('start_search', { start_search: inEl.value || '', page_url: location.href }); }); 1223 if (outEl) outEl.addEventListener('change', function(){ window.pxTrack('egreso_search', { egreso_search: outEl.value || '', page_url: location.href }); }); 1224 } 1225 1226 // ----- WhatsApp click tracking (GA4/dataLayer + Meta Pixel "Contact") ----- 1227 // El cliente pidió medir el clic en el botón de WhatsApp como una conversión más. 1228 // Se replica el patrón del resto de los eventos (window.pxTrack -> gtag + dataLayer) y, 1229 // si el POS tiene el pixel cargado (FacebookPixelID), se dispara el evento estándar "Contact". 1230 function wireWhatsappClick() { 1231 if (window.__px_whatsapp_wired) return; 1232 window.__px_whatsapp_wired = true; 1233 1234 var companyId = ''; 1235 var lastWaMs = 0; 1236 1237 function extractPhone(href) { 1238 try { 1239 var m = href.match(/[?&]phone=(\+?\d+)/i); 1240 if (m) return m[1]; 1241 m = href.match(/wa\.me\/(\+?\d+)/i); 1242 if (m) return m[1]; 1243 } catch (e) {} 1244 return ''; 1245 } 1246 1247 // Cualquier <a> hacia WhatsApp (api./web.whatsapp.com o wa.me), incluidos los botones 1248 // flotantes .wsp_floating (neo_social_links / standard_lp_motor) y #checkout_wsp_floating. 1249 function findWhatsappLink(el) { 1250 try { 1251 if (!el || !el.closest) return null; 1252 var a = el.closest('a'); 1253 if (!a) return null; 1254 var href = (a.getAttribute('href') || '').toLowerCase(); 1255 if (/(?:api\.|web\.)?whatsapp\.com|wa\.me/.test(href)) return a; 1256 if (a.closest('.wsp_floating') || a.closest('#checkout_wsp_floating')) return a; 1257 } catch (e) {} 1258 return null; 1259 } 1260 1261 // Delegado en captura: se dispara aunque el módulo re-renderice el botón (los .wsp_floating 1262 // se re-inyectan/mueven al body). target="_blank" mantiene la página viva para los beacons. 1263 document.addEventListener('click', function (e) { 1264 var link = findWhatsappLink(e.target); 1265 if (!link) return; 1266 var now = Date.now(); 1267 if (now - lastWaMs < 800) return; // dedupe de doble-disparo 1268 lastWaMs = now; 1269 1270 var href = link.getAttribute('href') || ''; 1271 var phone = extractPhone(href); 1272 var payload = { 1273 phone: phone, 1274 destination_url: href, 1275 pos: pxContext.pos || '', 1276 product_id: pxContext.product_id || '', 1277 product_name: pxContext.product_name || '', 1278 company_id: companyId || '', 1279 search_id: pxContext.search_id || '', 1280 currency: pxContext.currency || '', 1281 page_url: location.href, 1282 session_id: pxContext.session_id 1283 }; 1284 1285 // GA4 + dataLayer (Google y GTM) 1286 window.pxTrack('whatsapp_click', payload); 1287 1288 // Meta Pixel: evento estándar "Contact". fbq solo existe si el POS tiene FacebookPixelID 1289 // (se inicializa en Conversion_SearchOK.php / PageView), asà que este typeof es el guard. 1290 try { 1291 if (typeof window.fbq === 'function') { 1292 window.fbq('track', 'Contact', { content_name: 'whatsapp', content_category: 'whatsapp_click' }); 1293 } 1294 } catch (e) {} 1295 }, true); 1296 } 1297
1298 // Enrich context from hidden inputs present in LP/consultas templates 1299 function enhanceContextFromHiddenInputs() { 1300 var startRaw = (document.querySelector('#Start') || 1301 document.querySelector('#fechaingreso') || 1302 document.querySelector('[name="Start"]') || { }).value || ''; 1303 var endRaw = (document.querySelector('#End') || 1304 document.querySelector('#fechaegreso') || 1305 document.querySelector('[name="End"]') || { }).value || ''; 1306 var start = normalizeDate(startRaw); 1307 var end = normalizeDate(endRaw); 1308 var sid = (document.querySelector('#SearchID') || {}).value || ''; 1309 var gf = (document.querySelector('#GroupsForm') || {}).value || ''; 1310 var rooms = 0, adults = 0, children = 0; 1311 if (gf) { 1312 gf.split(';').forEach(function(groupStr){ 1313 if (!groupStr) return; 1314 rooms += 1; 1315 var parts = groupStr.split(':'); 1316 if (parts.length < 2) return; 1317 var trio = parts[1].split(','); 1318 var a = parseInt(trio[0] || '0', 10); 1319 adults += isNaN(a) ? 0 : a; 1320 var childrenAges = (trio[1] || '0'); 1321 if (childrenAges && childrenAges !== '0') { 1322 children += childrenAges.split('.').filter(Boolean).length; 1323 } 1324 }); 1325 } 1326 var updated = {}; 1327 if (start && start !== pxContext.start_date) { pxContext.start_date = start; updated.start_date = start; } 1328 if (end && end !== pxContext.end_date) { pxContext.end_date = end; updated.end_date = end; } 1329 if (sid && sid !== pxContext.search_id) { pxContext.search_id = sid; updated.search_id = sid; } 1330 if (adults && adults !== pxContext.adults) { pxContext.adults = adults; updated.adults = adults; } 1331 if (children && children !== pxContext.children) { pxContext.children = children; updated.children = children; } 1332 if (rooms && rooms !== pxContext.rooms) { pxContext.rooms = rooms; updated.rooms = rooms; } 1333 if (Object.keys(updated).length) { 1334 try { window.dataLayer.push(Object.assign({ event: 'px_context_update' }, updated)); } catch(e) {} 1335 } 1336 } 1337 1338 // Parse GroupsForm like "1:2,0,0;1:1,5.7,0" into {rooms, adults, children} 1339 function parseGroupsForm(gf) { 1340 var out = { rooms: 0, adults: 0, children: 0 };
1341 try { 1342 if (!gf) return out; 1343 String(gf).split(';').forEach(function(groupStr){ 1344 if (!groupStr) return; 1345 out.rooms += 1; 1346 var parts = groupStr.split(':'); 1347 if (parts.length < 2) return; 1348 var trio = parts[1].split(','); 1349 var a = parseInt(trio[0] || '0', 10); 1350 out.adults += isNaN(a) ? 0 : a; 1351 var childrenAges = (trio[1] || '0'); 1352 if (childrenAges && childrenAges !== '0') { 1353 out.children += childrenAges.split('.').filter(Boolean).length; 1354 } 1355 // Babies are not counted in children 1356 }); 1357 } catch (e) {} 1358 return out; 1359 } 1360 1361 // Enrich context from px_jsonQuery (server-rendered Json_Query) when available 1362 function enhanceContextFromPxJson() { 1363 try { 1364 var q = (typeof window !== 'undefined' && window.px_jsonQuery) ? window.px_jsonQuery : (typeof px_jsonQuery !== 'undefined' ? px_jsonQuery : null); 1365 if (!q) return; 1366 var updated = {}; 1367 // SearchID 1368 if (q.SearchID && String(q.SearchID) !== String(pxContext.search_id || '')) { 1369 pxContext.search_id = String(q.SearchID); 1370 updated.search_id = pxContext.search_id; 1371 } 1372 // Dates 1373 var start = normalizeDate(q.Start || ''); 1374 var end = normalizeDate(q.End || ''); 1375 if (start && start !== pxContext.start_date) { pxContext.start_date = start; updated.start_date = start; } 1376 if (end && end !== pxContext.end_date) { pxContext.end_date = end; updated.end_date = end; } 1377 // Pax/rooms from GroupsForm or Group array 1378 var paxParsed = null; 1379 if (q.GroupsForm) { 1380 paxParsed = parseGroupsForm(q.GroupsForm); 1381 } else if (q.Group && Array.isArray(q.Group)) { 1382 paxParsed = { rooms: 0, adults: 0, children: 0 }; 1383 q.Group.forEach(function(g){ 1384 paxParsed.rooms += 1; 1385 paxParsed.adults += (g.Adults*1) || 0; 1386 paxParsed.children += (g.Childs*1) || 0; 1387 }); 1388 } 1389 if (paxParsed) { 1390 if (paxParsed.adults && paxParsed.adults !== pxContext.adults) { pxContext.adults = paxParsed.adults; updated.adults = paxParsed.adults; } 1391 if (paxParsed.children && paxParsed.children !== pxContext.children) { pxContext.children = paxParsed.children; updated.children = paxParsed.children; } 1392 if (paxParsed.rooms && paxParsed.rooms !== pxContext.rooms) { pxContext.rooms = paxParsed.rooms; updated.rooms = paxParsed.rooms; } 1393 } 1394 if (Object.keys(updated).length) { 1395 try { window.dataLayer.push(Object.assign({ event: 'px_context_update' }, updated)); } catch(e) {} 1396 } 1397 } catch (e) {} 1398 } 1399 1400 // Process API payloads to enrich context 1401 function processApiData(url, data) { 1402 try { 1403 if (!data) return; 1404 // avoid reprocessing same SearchID repeatedly 1405 window.__px_processed_sids = window.__px_processed_sids || {}; 1406 var updated = {}; 1407 var payload = data && data.Json_Query ? data.Json_Query : data; 1408 // SearchID present? 1409 if (typeof payload.SearchID !== 'undefined' && String(payload.SearchID)) { 1410 var sid = String(payload.SearchID); 1411 if (window.__px_processed_sids[sid]) return; 1412 if (sid && sid !== String(pxContext.search_id || '')) { pxContext.search_id = sid; updated.search_id = sid; } 1413 window.__px_processed_sids[sid] = true; 1414 } 1415 // Start / End can exist either top-level or nested 1416 var start = payload.Start || (data && data.Start) || ''; 1417 var end = payload.End || (data && data.End) || ''; 1418 start = normalizeDate(start); 1419 end = normalizeDate(end); 1420 if (start && start !== pxContext.start_date) { pxContext.start_date = start; updated.start_date = start; } 1421 if (end && end !== pxContext.end_date) { pxContext.end_date = end; updated.end_date = end; } 1422 // GroupsForm could be present 1423 var gf = payload.GroupsForm || (data && data.GroupsForm) || ''; 1424 if (gf) { 1425 var parsed = parseGroupsForm(gf); 1426 if (parsed.adults && parsed.adults !== pxContext.adults) { pxContext.adults = parsed.adults; updated.adults = parsed.adults; } 1427 if (parsed.children && parsed.children !== pxContext.children) { pxContext.children = parsed.children; updated.children = parsed.children; } 1428 if (parsed.rooms && parsed.rooms !== pxContext.rooms) { pxContext.rooms = parsed.rooms; updated.rooms = parsed.rooms; } 1429 } 1430 // Some APIs include Group arrays; attempt totals 1431 if (payload.Group && Array.isArray(payload.Group)) { 1432 var aT = 0, cT = 0, rT = 0; 1433 payload.Group.forEach(function(g){ 1434 if (!g) return; 1435 rT += 1; 1436 aT += (g.Adults*1) || 0; 1437 cT += (g.Childs*1) || 0; 1438 }); 1439 if (aT && aT !== pxContext.adults) { pxContext.adults = aT; updated.adults = aT; } 1440 if (cT && cT !== pxContext.children) { pxContext.children = cT; updated.children = cT; } 1441 if (rT && rT !== pxContext.rooms) { pxContext.rooms = rT; updated.rooms = rT; } 1442 } 1443 if (Object.keys(updated).length) { 1444 try { window.dataLayer.push(Object.assign({ event: 'px_context_update' }, updated)); } catch(e) {} 1445 } 1446 } catch(e) {} 1447 } 1448 1449 // Intercept fetch/XHR to learn from API responses 1450 (function installNetworkTaps(){
1451 try { 1452 var origFetch = window.fetch; 1453 if (origFetch && !window.__px_fetch_hooked) { 1454 window.__px_fetch_hooked = true; 1455 window.fetch = function(input, init) { 1456 var p = origFetch(input, init); 1457 try { 1458 var url = typeof input === 'string' ? input : (input && input.url) || ''; 1459 p.then(function(res){ 1460 try { 1461 var ct = (res.headers && res.headers.get && res.headers.get('content-type')) || ''; 1462 if (ct && ct.indexOf('application/json') !== -1 && (/\/query\/list/i.test(url) || /\/search\/info/i.test(url))) { 1463 res.clone().json().then(function(json){ processApiData(url, json); }).catch(function(){}); 1464 } 1465 } catch(e) {} 1466 }).catch(function(){}); 1467 } catch(e) {} 1468 return p; 1469 }; 1470 } 1471 } catch(e) {} 1472 try { 1473 if (!window.__px_xhr_hooked && window.XMLHttpRequest) { 1474 window.__px_xhr_hooked = true; 1475 var XHR = window.XMLHttpRequest; 1476 var open = XHR.prototype.open; 1477 var send = XHR.prototype.send; 1478 XHR.prototype.open = function(method, url) { 1479 this.__px_url = url; 1480 return open.apply(this, arguments); 1481 }; 1482 XHR.prototype.send = function(body) { 1483 try { 1484 this.addEventListener('load', function() { 1485 try { 1486 var url = this.__px_url || ''; 1487 var ct = this.getResponseHeader && this.getResponseHeader('content-type') || ''; 1488 if (ct && ct.indexOf('application/json') !== -1 && (/\/query\/list/i.test(url) || /\/search\/info/i.test(url))) { 1489 var txt = this.responseText; 1490 try { processApiData(url, JSON.parse(txt)); } catch(e) {} 1491 } 1492 } catch(e) {} 1493 }); 1494 } catch(e) {} 1495 return send.apply(this, arguments); 1496 }; 1497 } 1498 } catch(e) {} 1499 })(); 1500 1501 // If we have SearchID but missing details, fetch /search/info 1502 function fetchSearchInfoIfNeeded() { 1503 try { 1504 var sid = pxContext.search_id || getUrlParam('SearchID') || (document.querySelector('#SearchID')||{}).value || ''; 1505 var needsDates = !(pxContext.start_date && pxContext.end_date); 1506 var needsPaxs = !((pxContext.adults*1) > 0 || (pxContext.children*1) > 0); 1507 if (sid && (needsDates || needsPaxs)) { 1508 fetchSearchInfoForSid(sid); 1509 } 1510 } catch(e) {} 1511 } 1512 1513 // Cached fetch per SearchID 1514 function fetchSearchInfoForSid(sid) { 1515 try { 1516 window.__px_sid_promises = window.__px_sid_promises || {}; 1517 if (window.__px_sid_promises[sid]) return window.__px_sid_promises[sid]; 1518 var endpoint = 'https://api-1-eb-web.pxsol.io/search/info?SearchID=' + encodeURIComponent(sid); 1519 var pr = fetch(endpoint, { credentials: 'omit' }).then(function(res){ 1520 if (!res.ok) return; 1521 var ct = (res.headers && res.headers.get && res.headers.get('content-type')) || ''; 1522 if (ct.indexOf('application/json') === -1) return; 1523 return res.json(); 1524 }).then(function(json){ 1525 if (json) processApiData(endpoint, json); 1526 return true; 1527 }).catch(function(){ return false; }); 1528 window.__px_sid_promises[sid] = pr; 1529 return pr; 1530 } catch(e) { return Promise.resolve(false); } 1531 } 1532 1533 // Ensure dates/paxs are ready before firing a callback 1534 function ensureContextReady(cb) { 1535 try { 1536 // Try quick enrichments first 1537 enhanceContextFromHiddenInputs(); 1538 enhanceContextFromPxJson(); 1539 var sid = pxContext.search_id || getUrlParam('SearchID') || (document.querySelector('#SearchID')||{}).value || ''; 1540 var hasDates = !!(pxContext.start_date && pxContext.end_date); 1541 var hasPaxs = ((pxContext.adults*1) > 0) || ((pxContext.children*1) > 0); 1542 if (hasDates && hasPaxs) { cb(); return; } 1543 if (sid) { 1544 fetchSearchInfoForSid(sid).finally(function(){ cb(); }); 1545 return; 1546 } 1547 // Last resort: try to enrich from DOM then run
1548 enhanceContextFromHiddenInputs(); 1549 cb(); 1550 } catch(e) { cb(); } 1551 } 1552 1553 // Detect "No hay resultados" to push no_inventory automatically 1554 function wireNoInventoryWatcher() { 1555 console.log('%c[pxTrack] ð Initializing no_inventory watcher...', 'color: #2196F3; font-weight: bold; font-size: 14px;'); 1556 console.log('[pxTrack] wireNoInventoryWatcher() called at:', new Date().toISOString()); 1557 var scanCount = 0; 1558 var lastScanResult = null; 1559 var lastSearchID = null; // Track SearchID to detect new searches 1560 var lastScanTime = 0; // For throttling scans 1561 var SCAN_THROTTLE_MS = 1000; // Maximum 1 scan per second 1562 var EVENT_COOLDOWN_MS = 5000; // Minimum 5 seconds between events of the same type 1563 var lastEventTime = {}; // Track last time each event type was fired 1564 var MAX_SCANS_PER_SEARCH = 30; // Maximum scans per search to prevent excessive polling 1565 var __px_has_inventory_pushed = false; // Flag para has_inventory (opuesto de no_inventory) 1566 1567 // Log immediately to confirm function is executing 1568 console.log('[pxTrack] â wireNoInventoryWatcher() is executing - scan function will be defined'); 1569 1570 // Expose scan function to global manual trigger 1571 window.__px_no_inventory_scan_func = function() { 1572 console.log('[pxTrack] Manual scan triggered via global function'); 1573 scan(); 1574 return lastScanResult; 1575 }; 1576 1577 function checkJsonQueryAvailability() { 1578 try { 1579 // Check if px_jsonQuery is available 1580 if (typeof window.px_jsonQuery === 'undefined' || !window.px_jsonQuery) { 1581 return false; 1582 } 1583 1584 var jsonQuery = window.px_jsonQuery; 1585 1586 // Log full structure on first check (for debugging) 1587 if (!window.__px_jsonQuery_logged) { 1588 window.__px_jsonQuery_logged = true; 1589 try { 1590 console.log('[pxTrack] ð¦ px_jsonQuery structure detected:', { 1591 hasProductList: !!(jsonQuery.ProductList), 1592 productListIsArray: Array.isArray(jsonQuery.ProductList), 1593 productListLength: jsonQuery.ProductList ? jsonQuery.ProductList.length : 0, 1594 sampleProduct: jsonQuery.ProductList && jsonQuery.ProductList.length > 0 ? { 1595 hasAvailability: typeof jsonQuery.ProductList[0].Availability !== 'undefined', 1596 availability: jsonQuery.ProductList[0].Availability, 1597 hasSkuList: !!(jsonQuery.ProductList[0].SkuList), 1598 skuListLength: jsonQuery.ProductList[0].SkuList ? jsonQuery.ProductList[0].SkuList.length : 0 1599 } : null 1600 }); 1601 // Log first product details if available 1602 if (jsonQuery.ProductList && jsonQuery.ProductList.length > 0) { 1603 var firstProduct = jsonQuery.ProductList[0]; 1604 console.log('[pxTrack] First product sample:', { 1605 Availability: firstProduct.Availability, 1606 hasSkuList: !!(firstProduct.SkuList), 1607 SkuList: firstProduct.SkuList ? firstProduct.SkuList.map(function(sku) { 1608 return { Availability: sku.Availability }; 1609 }).slice(0, 3) : null 1610 }); 1611 } 1612 } catch(e) { 1613 console.error('[pxTrack] Error logging Json_Query structure:', e); 1614 } 1615 } 1616 1617 // Check if ProductList exists and is an array 1618 if (!jsonQuery.ProductList) { 1619 // ProductList might not be loaded yet, but object exists 1620 return false; 1621 } 1622 1623 if (!Array.isArray(jsonQuery.ProductList)) { 1624 console.warn('[pxTrack] px_jsonQuery.ProductList exists but is not an array:', typeof jsonQuery.ProductList); 1625 return false; 1626 } 1627 1628 if (jsonQuery.ProductList.length === 0) { 1629 // Empty ProductList might indicate no inventory, but we need at least one product to check 1630 return false; 1631 } 1632 1633 // Check if ALL products have Availability === 0 1634 var allZeroAvailability = true; 1635 var availabilityDetails = []; 1636 1637 for (var i = 0; i < jsonQuery.ProductList.length; i++) { 1638 var product = jsonQuery.ProductList[i]; 1639 // Check Availability property (could be at product level or in SkuList) 1640 var productAvailability = product.Availability || 0; 1641 var productHasAvailability = typeof product.Availability !== 'undefined'; 1642 1643 // If product has SkuList, check all SKUs 1644 if (product.SkuList && Array.isArray(product.SkuList) && product.SkuList.length > 0) { 1645 var allSkusZero = true; 1646 var skuAvailabilities = []; 1647 for (var j = 0; j < product.SkuList.length; j++) { 1648 var sku = product.SkuList[j]; 1649 var skuAvail = sku.Availability || 0; 1650 skuAvailabilities.push(skuAvail); 1651 if (sku.Availability && sku.Availability > 0) { 1652 allSkusZero = false;
1653 break; 1654 } 1655 } 1656 availabilityDetails.push({ 1657 productIndex: i, 1658 hasProductAvailability: productHasAvailability, 1659 productAvailability: productAvailability, 1660 hasSkuList: true, 1661 skuAvailabilities: skuAvailabilities, 1662 allSkusZero: allSkusZero 1663 }); 1664 if (!allSkusZero) { 1665 allZeroAvailability = false; 1666 break; 1667 } 1668 } else { 1669 // Product-level availability check 1670 availabilityDetails.push({ 1671 productIndex: i, 1672 hasProductAvailability: productHasAvailability, 1673 productAvailability: productAvailability, 1674 hasSkuList: false 1675 }); 1676 if (productAvailability > 0) { 1677 allZeroAvailability = false; 1678 break; 1679 } 1680 } 1681 } 1682 1683 // Log availability details when checking (first time or when all zero) 1684 if (allZeroAvailability || !window.__px_availability_details_logged) { 1685 window.__px_availability_details_logged = true; 1686 console.log('[pxTrack] Availability check result:', { 1687 allZeroAvailability: allZeroAvailability, 1688 totalProducts: jsonQuery.ProductList.length, 1689 details: availabilityDetails.slice(0, 3) // Show first 3 products 1690 }); 1691 } 1692 1693 return allZeroAvailability; 1694 } catch(e) { 1695 console.error('[pxTrack] Error checking Json_Query availability:', e); 1696 return false; 1697 } 1698 } 1699 1700 function checkJsonQueryHasAvailability() { 1701 try { 1702 // Check if px_jsonQuery is available 1703 if (typeof window.px_jsonQuery === 'undefined' || !window.px_jsonQuery) { 1704 return { hasAvailability: false, totalAvailability: 0, availabilityDetails: null }; 1705 } 1706 1707 var jsonQuery = window.px_jsonQuery; 1708 1709 // Check if ProductList exists and is an array 1710 if (!jsonQuery.ProductList || !Array.isArray(jsonQuery.ProductList) || jsonQuery.ProductList.length === 0) { 1711 return { hasAvailability: false, totalAvailability: 0, availabilityDetails: null }; 1712 } 1713 1714 var totalAvailability = 0; 1715 var hasAnyAvailability = false; 1716 var availabilityDetails = []; 1717 1718 for (var i = 0; i < jsonQuery.ProductList.length; i++) { 1719 var product = jsonQuery.ProductList[i]; 1720 var productAvailability = product.Availability || 0; 1721 var productTotal = 0; 1722 1723 // If product has SkuList, sum all SKU availabilities 1724 if (product.SkuList && Array.isArray(product.SkuList) && product.SkuList.length > 0) { 1725 for (var j = 0; j < product.SkuList.length; j++) { 1726 var sku = product.SkuList[j]; 1727 var skuAvail = sku.Availability || 0; 1728 productTotal += skuAvail; 1729 if (skuAvail > 0) { 1730 hasAnyAvailability = true; 1731 } 1732 } 1733 } else { 1734 // Product-level availability 1735 productTotal = productAvailability; 1736 if (productAvailability > 0) { 1737 hasAnyAvailability = true; 1738 } 1739 } 1740 1741 totalAvailability += productTotal; 1742 availabilityDetails.push({ 1743 productIndex: i, 1744 productAvailability: productAvailability, 1745 productTotal: productTotal, 1746 hasSkuList: !!(product.SkuList && Array.isArray(product.SkuList) && product.SkuList.length > 0) 1747 }); 1748 } 1749 1750 return { 1751 hasAvailability: hasAnyAvailability, 1752 totalAvailability: totalAvailability, 1753 availabilityDetails: availabilityDetails, 1754 productCount: jsonQuery.ProductList.length 1755 }; 1756 } catch(e) { 1757 console.error('[pxTrack] Error checking Json_Query for availability:', e); 1758 return { hasAvailability: false, totalAvailability: 0, availabilityDetails: null }; 1759 } 1760 } 1761 1762 function scan() { 1763 // THROTTLING: Evitar escaneos muy frecuentes 1764 var now = Date.now(); 1765 if (now - lastScanTime < SCAN_THROTTLE_MS) { 1766 console.debug('[pxTrack] scan() throttled (too soon after last scan)'); 1767 return; 1768 } 1769 lastScanTime = now; 1770 1771 // Check if SearchID changed (new search) - if so, reset the flag 1772 try { 1773 var currentSearchID = null; 1774 if (typeof window.px_jsonQuery !== 'undefined' && window.px_jsonQuery && window.px_jsonQuery.SearchID) { 1775 currentSearchID = String(window.px_jsonQuery.SearchID); 1776 } else { 1777 var searchIDEl = document.querySelector('#SearchID'); 1778 if (searchIDEl && searchIDEl.value) { 1779 currentSearchID = String(searchIDEl.value); 1780 } else { 1781 try {
1782 var urlParams = new URLSearchParams(location.search); 1783 currentSearchID = urlParams.get('SearchID') || null; 1784 } catch(e) {} 1785 } 1786 } 1787 1788 // If SearchID changed, reset the flags for new search 1789 if (currentSearchID && currentSearchID !== lastSearchID && lastSearchID !== null) { 1790 console.log('[pxTrack] ð New search detected (SearchID: ' + lastSearchID + ' â ' + currentSearchID + '), resetting flags'); 1791 window.__px_no_inventory_pushed = false; 1792 __px_has_inventory_pushed = false; 1793 scanCount = 0; // Reset scan count 1794 lastScanTime = 0; // Reset throttling for new search 1795 } 1796 1797 // Update lastSearchID 1798 if (currentSearchID) { 1799 lastSearchID = currentSearchID; 1800 } 1801 } catch(e) { 1802 console.error('[pxTrack] Error checking SearchID:', e); 1803 } 1804 1805 // Don't scan if event already fired (unless we just reset it) 1806 if (window.__px_no_inventory_pushed) { 1807 console.debug('[pxTrack] scan() called but event already fired, skipping'); 1808 return; 1809 } 1810 1811 // LÃmite máximo de escaneos por búsqueda 1812 if (scanCount >= MAX_SCANS_PER_SEARCH) { 1813 console.debug('[pxTrack] scan() skipped - maximum scans per search reached (' + MAX_SCANS_PER_SEARCH + ')'); 1814 return; 1815 } 1816 1817 scanCount++; 1818 // Log first few scans to confirm function is working 1819 if (scanCount <= 5) { 1820 console.log('[pxTrack] scan() executing - Scan #' + scanCount); 1821 } 1822 var shouldFire = false; 1823 var detectionMethod = ''; 1824 var debugInfo = { 1825 scanNumber: scanCount, 1826 px_jsonQuery_exists: typeof window.px_jsonQuery !== 'undefined' && !!window.px_jsonQuery, 1827 productList_length: 0, 1828 productList_details: [] 1829 }; 1830 1831 // Method 1: Check px_jsonQuery.ProductList for all Availability === 0 1832 try { 1833 if (typeof window.px_jsonQuery !== 'undefined' && window.px_jsonQuery) { 1834 debugInfo.productList_length = (window.px_jsonQuery.ProductList && Array.isArray(window.px_jsonQuery.ProductList)) ? window.px_jsonQuery.ProductList.length : 0; 1835 1836 if (checkJsonQueryAvailability()) { 1837 shouldFire = true; 1838 detectionMethod = 'Json_Query'; 1839 try { 1840 console.log('%c[pxTrack] â no_inventory DETECTED via Json_Query (all Availability === 0) - Scan #' + scanCount, 'color: #FF5722; font-weight: bold;'); 1841 } catch(_e){} 1842 } else { 1843 // Log details about why it didn't fire 1844 try { 1845 if (window.px_jsonQuery.ProductList && Array.isArray(window.px_jsonQuery.ProductList)) { 1846 var productDetails = []; 1847 for (var idx = 0; idx < Math.min(window.px_jsonQuery.ProductList.length, 3); idx++) { 1848 var p = window.px_jsonQuery.ProductList[idx]; 1849 var prodAvail = p.Availability || 0; 1850 var skuAvail = 'N/A'; 1851 if (p.SkuList && Array.isArray(p.SkuList) && p.SkuList.length > 0) { 1852 var skuAvails = []; 1853 for (var skuIdx = 0; skuIdx < p.SkuList.length; skuIdx++) { 1854 skuAvails.push(p.SkuList[skuIdx].Availability || 0); 1855 } 1856 skuAvail = skuAvails.join(','); 1857 } 1858 productDetails.push({productAvail: prodAvail, skuAvail: skuAvail}); 1859 } 1860 debugInfo.productList_details = productDetails; 1861 } 1862 } catch(e) {} 1863 } 1864 } 1865 } catch(e) { 1866 console.error('[pxTrack] Error in Json_Query availability check:', e); 1867 } 1868 1869 // Method 2: Check for specific DOM elements (most reliable) 1870 if (!shouldFire) { 1871 try { 1872 // Check for #no-inventory-container or #no-inventory (from sindispo.php) 1873 var noInventoryContainer = document.querySelector('#no-inventory-container'); 1874 var noInventoryElement = document.querySelector('#no-inventory'); 1875 if (noInventoryContainer || noInventoryElement) { 1876 shouldFire = true; 1877 detectionMethod = 'DOM element (ID)'; 1878 try { 1879 console.log('%c[pxTrack] â no_inventory DETECTED via DOM element (#no-inventory-container or #no-inventory) - Scan #' + scanCount, 'color: #FF5722; font-weight: bold;'); 1880 console.log('[pxTrack] DOM elements found:', { 1881 hasContainer: !!noInventoryContainer, 1882 hasElement: !!noInventoryElement, 1883 containerVisible: noInventoryContainer ? (noInventoryContainer.offsetParent !== null) : false, 1884 elementText: noInventoryElement ? (noInventoryElement.textContent || noInventoryElement.innerText || '').substring(0, 50) : '' 1885 }); 1886 } catch(_e){} 1887 } else if (scanCount <= 3 || scanCount % 10 === 0) { 1888 // Log when elements are not found (for debugging) 1889 try { 1890 console.debug('[pxTrack] DOM check: #no-inventory-container and #no-inventory not found (scan #' + scanCount + ')'); 1891 } catch(_e){} 1892 } 1893 } catch(e) { 1894 console.error('[pxTrack] Error in DOM element check:', e); 1895 } 1896 } 1897 1898 // Method 3: Fallback - Check DOM text for "No hay resultados" or "No results" 1899 if (!shouldFire) { 1900 try { 1901 var candidates = Array.prototype.slice.call(document.querySelectorAll('body *')); 1902 for (var i=0; i<candidates.length; i++) { 1903 var t = (candidates[i].innerText || '').trim(); 1904 if (!t) continue; 1905 if (t.indexOf('No hay resultados') !== -1 || t.indexOf('No results') !== -1) { 1906 shouldFire = true; 1907 detectionMethod = 'DOM text'; 1908 try { 1909 console.log('%c[pxTrack] â no_inventory DETECTED via DOM text - Scan #' + scanCount, 'color: #FF5722; font-weight: bold;'); 1910 } catch(_e){} 1911 break; 1912 } 1913 } 1914 } catch(e) { 1915 console.error('[pxTrack] Error in DOM text check:', e); 1916 } 1917 } 1918 1919 // Store last scan result for debugging 1920 lastScanResult = { 1921 shouldFire: shouldFire, 1922 method: detectionMethod, 1923 scanCount: scanCount, 1924 px_jsonQuery_exists: debugInfo.px_jsonQuery_exists, 1925 productList_length: debugInfo.productList_length, 1926 productList_details: debugInfo.productList_details 1927 }; 1928 1929 // Log scan details every 5th scan or when px_jsonQuery becomes available 1930 if (scanCount % 5 === 0 || (debugInfo.px_jsonQuery_exists && scanCount <= 3)) { 1931 try { 1932 console.log('[pxTrack] no_inventory scan #' + scanCount + ':', { 1933 px_jsonQuery_exists: debugInfo.px_jsonQuery_exists, 1934 productList_length: debugInfo.productList_length, 1935 shouldFire: shouldFire, 1936 method: detectionMethod 1937 }); 1938 if (debugInfo.productList_details.length > 0) { 1939 console.log('[pxTrack] Sample product availability:', debugInfo.productList_details); 1940 } 1941 } catch(_e){} 1942 } 1943 1944 if (shouldFire && !window.__px_no_inventory_pushed) { 1945 // COOLDOWN: Verificar que no se haya disparado recientemente 1946 var eventType = 'no_inventory'; 1947 var now = Date.now(); 1948 if (lastEventTime[eventType] && (now - lastEventTime[eventType] < EVENT_COOLDOWN_MS)) { 1949 console.log('[pxTrack] â¸ï¸ Event cooldown active, skipping no_inventory event (last fired ' + Math.round((now - lastEventTime[eventType]) / 1000) + 's ago)'); 1950 return; 1951 } 1952 1953 window.__px_no_inventory_pushed = true; 1954 lastEventTime[eventType] = now; // Registrar tiempo del evento 1955 1956 // Ensure dates/paxs are enriched before sending 1957 ensureContextReady(function(){ 1958 var params = getSearchParamsFromDOM(); 1959 params.no_inventory = true; 1960 try { 1961 console.log('%c[pxTrack] ð FIRING no_inventory event - Detection method: ' + detectionMethod + ' - Scan #' + scanCount, 'color: #4CAF50; font-weight: bold; font-size: 14px;'); 1962 console.log('[pxTrack] no_inventory payload:', params); 1963 console.log('[pxTrack] Last scan result:', lastScanResult); 1964 } catch(_e){} 1965 window.pxTrack('no_inventory', params); 1966 1967 // Fire form_success for search form after no_inventory 1968 setTimeout(function() { 1969 console.log('%c[pxTrack] ð FIRING form_success for search form (after no_inventory)', 'color: #4CAF50; font-weight: bold;'); 1970 window.pxTrack('form_success', { 1971 form_id: 'pxmk_searchform', 1972 lead_id: null, 1973 transaction_id: null 1974 }); 1975 }, 100); 1976 }); 1977 } 1978 1979 // OPPOSITE: Check if there IS availability (has_inventory) 1980 // Only check if no_inventory was NOT detected and has_inventory hasn't been fired yet 1981 if (!shouldFire && !__px_has_inventory_pushed && !window.__px_no_inventory_pushed) { 1982 try { 1983 // Also verify that no "no inventory" DOM elements are present 1984 var noInventoryContainer = document.querySelector('#no-inventory-container'); 1985 var noInventoryElement = document.querySelector('#no-inventory'); 1986 var hasNoInventoryDOM = !!(noInventoryContainer || noInventoryElement); 1987 1988 if (hasNoInventoryDOM) { 1989 console.debug('[pxTrack] has_inventory blocked - no inventory DOM elements present'); 1990 } else { 1991 var availabilityInfo = checkJsonQueryHasAvailability(); 1992 1993 if (availabilityInfo.hasAvailability) { 1994 // COOLDOWN: Verificar que no se haya disparado recientemente 1995 var eventType = 'has_inventory'; 1996 var now = Date.now(); 1997 if (lastEventTime[eventType] && (now - lastEventTime[eventType] < EVENT_COOLDOWN_MS)) { 1998 console.log('[pxTrack] â¸ï¸ Event cooldown active, skipping has_inventory event (last fired ' + Math.round((now - lastEventTime[eventType]) / 1000) + 's ago)'); 1999 } else { 2000 2001 __px_has_inventory_pushed = true; 2002 lastEventTime[eventType] = now; 2003 2004 // Ensure dates/paxs are enriched before sending 2005 ensureContextReady(function(){ 2006 var params = getSearchParamsFromDOM(); 2007 params.has_availability = true; 2008 params.availability = availabilityInfo.totalAvailability; 2009 params.product_count = availabilityInfo.productC
2009ount || 0; 2010 2011 // Add SearchID if available 2012 try { 2013 if (typeof window.px_jsonQuery !== 'undefined' && window.px_jsonQuery && window.px_jsonQuery.SearchID) { 2014 params.search_id = String(window.px_jsonQuery.SearchID); 2015 } 2016 } catch(e) {} 2017 2018 // Add availability details (first 3 products for debugging) 2019 if (availabilityInfo.availabilityDetails && availabilityInfo.availabilityDetails.length > 0) { 2020 params.availability_details = availabilityInfo.availabilityDetails.slice(0, 3).map(function(detail) { 2021 return { 2022 product_index: detail.productIndex, 2023 availability: detail.productTotal, 2024 has_skus: detail.hasSkuList 2025 }; 2026 }); 2027 } 2028 2029 try { 2030 console.log('%c[pxTrack] ð FIRING has_inventory event (availability detected) - Scan #' + scanCount, 'color: #4CAF50; font-weight: bold; font-size: 14px;'); 2031 console.log('[pxTrack] has_inventory payload:', params); 2032 console.log('[pxTrack] Availability info:', { 2033 totalAvailability: availabilityInfo.totalAvailability, 2034 productCount: availabilityInfo.productCount, 2035 hasAvailability: availabilityInfo.hasAvailability 2036 }); 2037 } catch(_e){} 2038 window.pxTrack('has_inventory', params); 2039 2040 // Fire form_success for search form after has_inventory 2041 setTimeout(function() { 2042 console.log('%c[pxTrack] ð FIRING form_success for search form (after has_inventory)', 'color: #4CAF50; font-weight: bold;'); 2043 window.pxTrack('form_success', { 2044 form_id: 'pxmk_searchform', 2045 lead_id: null, 2046 transaction_id: null 2047 }); 2048 }, 100); 2049 }); 2050 } 2051 } 2052 } 2053 } catch(e) { 2054 console.error('[pxTrack] Error checking for availability (has_inventory):', e); 2055 } 2056 } 2057 } 2058 2059 // Try to intercept px_jsonQuery assignment (works if px_jsonQuery is undefined) 2060 try { 2061 if (typeof window.px_jsonQuery === 'undefined') { 2062 try { 2063 var _px_jsonQuery_value = undefined; 2064 var _px_jsonQuery_lastTrigger = 0; 2065 Object.defineProperty(window, 'px_jsonQuery', { 2066 get: function() { return _px_jsonQuery_value; }, 2067 set: function(newValue) { 2068 _px_jsonQuery_value = newValue; 2069 if (newValue) { 2070 try { 2071 // Throttle interceptor triggers (máximo 1 cada 1 segundo) 2072 var now = Date.now(); 2073 if (now - _px_jsonQuery_lastTrigger < 1000) { 2074 console.debug('[pxTrack] px_jsonQuery interceptor throttled'); 2075 return; 2076 } 2077 _px_jsonQuery_lastTrigger = now; 2078 console.log('[pxTrack] px_jsonQuery was set via Object.defineProperty, triggering scan'); 2079 setTimeout(function() { scan(); }, 150); 2080 } catch(e) { 2081 console.error('[pxTrack] Error in triggerScan:', e); 2082 } 2083 } 2084 }, 2085 configurable: true, 2086 enumerable: true 2087 }); 2088 } catch(e) { 2089 console.error('[pxTrack] Object.defineProperty failed for px_jsonQuery:', e); 2090 } 2091 } 2092 } catch(e) { 2093 console.error('[pxTrack] Error setting up px_jsonQuery interception:', e); 2094 } 2095 2096 // Multiple initial scans with increasing delays (for Chrome compatibility) 2097 function runInitialScans() { 2098 console.log('%c[pxTrack] Starting initial scans for no_inventory detection...', 'color: #FF9800; font-weight: bold;'); 2099 console.log('[pxTrack] runInitialScans() called - will execute multiple scans'); 2100 // Immediate scan 2101 console.log('[pxTrack] Executing immediate scan...'); 2102 scan(); 2103 // Delayed scans to catch px_jsonQuery if it loads later AND DOM elements that load dynamically 2104 setTimeout(function() { scan(); }, 200); 2105 setTimeout(function() { scan(); }, 500); 2106 setTimeout(function() { scan(); }, 1000); 2107 setTimeout(function() { scan(); }, 2000); 2108 setTimeout(function() { scan(); }, 3000); 2109 setTimeout(function() { scan(); }, 5000); 2110 } 2111 2112 // Run initial scans after DOM is ready 2113 if (document.readyState === 'loading') { 2114 console.log('[pxTrack] DOM is loading, will wait for DOMContentLoaded'); 2115 document.addEventListener('DOMContentLoaded', function() { 2116 console.log('[pxTrack] DOMContentLoaded fired, scheduling runInitialScans() in 100ms'); 2117 setTimeout(function() { 2118 console.log('[pxTrack] Executing runInitialScans() now...'); 2119 runInitialScans(); 2120 }, 100); 2121 }); 2122 } else { 2123 console.log('[pxTrack] DOM already ready, scheduling runInitialScans() in 100ms'); 2124 setTimeout(function() { 2125 console.log('[pxTrack] Executing runInitialScans() now...'); 2126 runInitialScans(); 2127 }, 100); 2128 } 2129 2130 // Also use requestAnimationFrame for Chrome (ensures DOM is painted) 2131 if (window.requestAnimationFrame) { 2132 requestAnimationFrame(function() { 2133 setTimeout(function() { scan(); }, 500); 2134 }); 2135 } 2136 2137 // Watch for px_jsonQuery availability (reduced polling frequency to prevent rate limiting) 2138 var jsonQueryCheckStart = Date.now(); 2139 var jsonQueryCheckTimeout = 20000; // 20 seconds 2140 var jsonQueryCheckInterval = setInterval(function() { 2141 // Stop if timeout reached 2142 if (Date.now() - jsonQueryCheckStart > jsonQueryCheckTimeout) { 2143 clearInterval(jsonQueryCheckInterval); 2144 try { 2145 console.log('[pxTrack] no_inventory px_jsonQuery polling timed out after', scanCount, 'scans'); 2146 console.log('[pxTrack] Last scan result:', lastScanResult); 2147 } catch(_e){} 2148 return; 2149 } 2150 2151 // Stop if event already fired 2152 if (window.__px_no_inventory_pushed) { 2153 clearInterval(jsonQueryCheckInterval); 2154 return; 2155 } 2156 2157 // Check if px_jsonQuery is now available and scan 2158 if (typeof window.px_jsonQuery !== 'undefined' && window.px_jsonQuery) { 2159 scan(); 2160 } 2161 }, 1000); // Reduced to 1 second to prevent rate limiting 2162 2163 // Watch DOM changes and specifically look for #no-inventory-container or #no-inventory
2164 try { 2165 var mo = new MutationObserver(function(mutations) { 2166 if (window.__px_no_inventory_pushed) return; 2167 2168 // Throttling más agresivo para MutationObserver (2 segundos mÃnimo) 2169 var now = Date.now(); 2170 if (mo.__px_lastScan && (now - mo.__px_lastScan < 2000)) { 2171 return; 2172 } 2173 2174 // Check if any of the mutations added our target elements 2175 var foundTarget = false; 2176 for (var i = 0; i < mutations.length; i++) { 2177 var mutation = mutations[i]; 2178 if (mutation.addedNodes && mutation.addedNodes.length > 0) { 2179 for (var j = 0; j < mutation.addedNodes.length; j++) { 2180 var node = mutation.addedNodes[j]; 2181 if (node.nodeType === 1) { // Element node 2182 // Check if the added node is our target or contains it 2183 if (node.id === 'no-inventory-container' || node.id === 'no-inventory' || 2184 node.querySelector && (node.querySelector('#no-inventory-container') || node.querySelector('#no-inventory'))) { 2185 foundTarget = true; 2186 break; 2187 } 2188 } 2189 } 2190 } 2191 if (foundTarget) break; 2192 } 2193 2194 // If target found or if enough time passed, scan 2195 if (foundTarget || !mo.__px_lastScan || Date.now() - mo.__px_lastScan > 2000) { 2196 mo.__px_lastScan = Date.now(); 2197 if (foundTarget) { 2198 console.log('[pxTrack] MutationObserver detected #no-inventory-container or #no-inventory added to DOM'); 2199 } 2200 scan(); 2201 } 2202 }); 2203 mo.observe(document.body, { childList: true, subtree: true }); 2204 } catch(e) { 2205 console.error('[pxTrack] MutationObserver failed for no_inventory:', e); 2206 } 2207 2208 // Additional polling specifically for DOM elements (reduced frequency to prevent rate limiting) 2209 var domPollingStart = Date.now(); 2210 var domPollingTimeout = 15000; // 15 seconds 2211 var domPollingInterval = setInterval(function() { 2212 if (Date.now() - domPollingStart > domPollingTimeout) { 2213 clearInterval(domPollingInterval); 2214 return; 2215 } 2216 2217 if (window.__px_no_inventory_pushed) { 2218 clearInterval(domPollingInterval); 2219 return; 2220 } 2221 2222 // Quick check for target elements 2223 var noInventoryContainer = document.querySelector('#no-inventory-container'); 2224 var noInventoryElement = document.querySelector('#no-inventory'); 2225 if (noInventoryContainer || noInventoryElement) { 2226 console.log('[pxTrack] DOM polling detected #no-inventory-container or #no-inventory'); 2227 scan(); 2228 clearInterval(domPollingInterval); 2229 } 2230 }, 1000); // Reduced to 1 second to prevent rate limiting 2231 2232 // Confirm watcher is fully initialized 2233 console.log('%c[pxTrack] â no_inventory watcher fully initialized and ready', 'color: #4CAF50; font-weight: bold; font-size: 14px;'); 2234 console.log('[pxTrack] wireNoInventoryWatcher() completed at:', new Date().toISOString()); 2235 console.log('[pxTrack] All watchers, observers, and polling intervals are now active'); 2236 } 2237 2238 // Observe availability containers to re-enrich context after dynamic updates 2239 function observeAvailabilityContainers() { 2240 try { 2241 var targets = []; 2242 var el1 = document.querySelector('#neo_cart_results_list'); 2243 var el2 = document.querySelector('#neo_cart_pedido_container'); 2244 if (el1) targets.push(el1); 2245 if (el2) targets.push(el2); 2246 if (!targets.length || !('MutationObserver' in window)) return; 2247 var refresh = function() { 2248 enhanceContextFromHiddenInputs(); 2249 enhanceContextFromPxJson(); 2250 }; 2251 var mo = new MutationObserver(function(muts) { 2252 // throttle bursts 2253 if (mo.__px_scheduled) return; 2254 mo.__px_scheduled = true;
2255 setTimeout(function(){ mo.__px_scheduled = false; refresh(); }, 100); 2256 }); 2257 targets.forEach(function(t){ mo.observe(t, { childList: true, subtree: true }); }); 2258 // initial pass 2259 refresh(); 2260 } catch (e) {} 2261 } 2262 2263 // Init after DOM ready - LOG EARLY AND OFTEN 2264 console.log('%c[pxTrack] STEP 1: Checking DOM readyState...', 'color: #9C27B0; font-weight: bold; font-size: 14px;'); 2265 console.log('[pxTrack] document.readyState:', document.readyState); 2266 console.log('[pxTrack] wireNoInventoryWatcher function exists:', typeof wireNoInventoryWatcher); 2267 2268 if (document.readyState === 'loading') { 2269 console.log('%c[pxTrack] STEP 2: DOM is loading, setting up DOMContentLoaded listener', 'color: #9C27B0; font-weight: bold;'); 2270 document.addEventListener('DOMContentLoaded', function() { 2271 console.log('%c[pxTrack] STEP 3: DOMContentLoaded fired!', 'color: #9C27B0; font-weight: bold; font-size: 16px;'); 2272 console.log('[pxTrack] DOMContentLoaded fired, initializing watchers...'); 2273 try { 2274 wireSearchFallbacks(); 2275 wireSearchEventsWhenReady(); 2276 wireFormEvents(); 2277 wireWhatsappClick(); 2278 if (typeof wireCheckoutForm === 'function') { 2279 wireCheckoutForm(); 2280 } else { 2281 console.log('[pxTrack] wireCheckoutForm() not available (may be conditional)'); 2282 } 2283 wireDatePickers(); 2284 enhanceContextFromHiddenInputs(); 2285 enhanceContextFromPxJson(); 2286 fetchSearchInfoIfNeeded(); 2287 console.log('%c[pxTrack] STEP 4: About to call wireNoInventoryWatcher()...', 'color: #FF5722; font-weight: bold; font-size: 16px;'); 2288 try { 2289 wireNoInventoryWatcher(); 2290 console.log('%c[pxTrack] STEP 5: wireNoInventoryWatcher() completed successfully!', 'color: #4CAF50; font-weight: bold; font-size: 16px;'); 2291 } catch(e) { 2292 console.error('[pxTrack] ERROR calling wireNoInventoryWatcher():', e); 2293 console.error('[pxTrack] Error stack:', e.stack); 2294 } 2295 observeAvailabilityContainers(); 2296 // delayed retries in case modules/scripts load late 2297 setTimeout(function(){ enhanceContextFromHiddenInputs(); enhanceContextFromPxJson(); }, 500); 2298 setTimeout(function(){ enhanceContextFromHiddenInputs(); enhanceContextFromPxJson(); }, 1500); 2299 // checkout success detector (thank-you page) - multiple attempts with delays 2300 if (typeof maybePushFormSuccess === 'function') { 2301 maybePushFormSuccess(); // Immediate 2302 setTimeout(function(){ maybePushFormSuccess(); }, 500); // After 500ms 2303 setTimeout(function(){ maybePushFormSuccess(); }, 1500); // After 1.5s 2304 setTimeout(function(){ maybePushFormSuccess(); }, 3000); // After 3s (for slow loading pages) 2305 } 2306 } catch(e) { 2307 console.error('[pxTrack] Error during initialization:', e); 2308 } 2309 }); 2310 } else { 2311 console.log('%c[pxTrack] STEP 2: DOM already ready, initializing immediately', 'color: #9C27B0; font-weight: bold;'); 2312 console.log('[pxTrack] DOM already ready, initializing watchers immediately...'); 2313 try { 2314 wireSearchFallbacks(); 2315 wireSearchEventsWhenReady(); 2316 wireFormEvents(); 2317 wireWhatsappClick(); 2318 if (typeof wireCheckoutForm === 'function') { 2319 wireCheckoutForm(); 2320 } else { 2321 console.log('[pxTrack] wireCheckoutForm() not available (may be conditional)'); 2322 } 2323 wireDatePickers(); 2324 enhanceContextFromHiddenInputs(); 2325 enhanceContextFromPxJson(); 2326 fetchSearchInfoIfNeeded(); 2327 console.log('%c[pxTrack] STEP 4: About to call wireNoInventoryWatcher()...', 'color: #FF5722; font-weight: bold; font-size: 16px;'); 2328 try { 2329 wireNoInventoryWatcher(); 2330 console.log('%c[pxTrack] STEP 5: wireNoInventoryWatcher() completed successfully!', 'color: #4CAF50; font-weight: bold; font-size: 16px;'); 2331 } catch(e) { 2332 console.error('[pxTrack] ERROR calling wireNoInventoryWatcher():', e); 2333 console.error('[pxTrack] Error stack:', e.stack); 2334 } 2335 observeAvailabilityContainers();
2336 setTimeout(function(){ enhanceContextFromHiddenInputs(); enhanceContextFromPxJson(); }, 500); 2337 setTimeout(function(){ enhanceContextFromHiddenInputs(); enhanceContextFromPxJson(); }, 1500); 2338 // checkout success detector (thank-you page) - multiple attempts with delays 2339 if (typeof maybePushFormSuccess === 'function') { 2340 maybePushFormSuccess(); // Immediate 2341 setTimeout(function(){ maybePushFormSuccess(); }, 500); // After 500ms 2342 setTimeout(function(){ maybePushFormSuccess(); }, 1500); // After 1.5s 2343 setTimeout(function(){ maybePushFormSuccess(); }, 3000); // After 3s (for slow loading pages) 2344 } 2345 } catch(e) { 2346 console.error('[pxTrack] Error during initialization:', e); 2347 console.error('[pxTrack] Error stack:', e.stack); 2348 } 2349 } 2350 })(); 2351</script>
2351 2352 2353 2354<!-- Google Tag Manager -->
2355<script>
vendor: 436 bytes, lines 2355-2369
2355 2356 (function(w, d, s, l, i) { 2357 w[l] = w[l] || []; 2358 w[l].push({ 2359 'gtm.start': new Date().getTime(), 2360 event: 'gtm.js' 2361 }); 2362 var f = d.getElementsByTagName(s)[0], 2363 j = d.createElement(s), 2364 dl = l != 'dataLayer' ? '&l=' + l : ''; 2365 j.async = true; 2366 j.src = 2367 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; 2368 f.parentNode.insertBefore(j, f); 2369 })(window, document, 'script', 'dataLayer', '
2369GTM-TPTNCNX
vendor: 4 bytes, line 2369
2369');
2370</script>
2370 2371<!-- End Google Tag Manager --> 2372 2373 <!-- Google Tag Manager --> 2374
2374<script> 2375 (function(w, d, s, l, i) { 2376 w[l] = w[l] || []; 2377 w[l].push({ 2378 'gtm.start': new Date().getTime(), 2379 event: 'gtm.js' 2380 }); 2381 var f = d.getElementsByTagName(s)[0], 2382 j = d.createElement(s), 2383 dl = l != 'dataLayer' ? '&l=' + l : ''; 2384 j.async = true; 2385 j.src = 2386 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; 2387 f.parentNode.insertBefore(j, f); 2388 })(window, document, 'script', 'dataLayer', 'AW-824953249'); 2389 </script>
2389 2390 <!-- End Google Tag Manager --> 2391 2392 2393 2394 2395 <!-- Organization Schema for Brand/Logo SEO --> 2396
2396<script type="application/ld+json"> 2397 { 2398 "@context": "https://schema.org", 2399 "@type": "Organization", 2400 "name": "HSR TERMAS", 2401 "url": "https://www.sanremohoteles.com/list.html?mkt_active=true&Pid=24471&origin=marketplace_homepage", 2402 "logo": "https://images-new.pxsol.com/BJEPhNpVdEnVxWSk3IB3nOgtC4ZnU8fWWDVAfHZd6O8/rs:fill:512:512:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png", 2403 "image": "https://images-new.pxsol.com/BJEPhNpVdEnVxWSk3IB3nOgtC4ZnU8fWWDVAfHZd6O8/rs:fill:512:512:1/q:80/plain/https%3A%2F%2Ffiles-p.pxsol.com%2F13307%2Fcompany%2Flibrary%2Fuser%2F5126930364ea8a9a40b647af1c2cabae351ef5e772.png@png" 2404 } 2405 </script>
2405 2406 2407</head> 2408 2409<body> 2410 2411 2412 <div class="neo_loader"> 2413<style> 2414.linear-activity{overflow:hidden;width:100%;height:4px;background-color:#B3E5FC;margin:0}.determinate{position:relative;max-width:100%;height:100%;-webkit-transition:width .5s ease-out 1s;-moz-transition:width .5s ease-out 1s;-o-transition:width .5s ease-out 1s;transition:width .5s ease-out 1s;background-color:#03A9F4}.indeterminate{position:relative;width:100%;height:100%}.indeterminate:after,.indeterminate:before{content:'';position:absolute;height:100%}.indeterminate:before{background-color:#03A9F4;animation:indeterminate_first 1.5s infinite ease-out}.indeterminate:after{background-color:#4FC3F7;animation:indeterminate_second 1.5s infinite ease-in}@keyframes indeterminate_first{0%{left:-100%;width:100%}100%{left:100%;width:10%}}@keyframes indeterminate_second{0%{left:-150%;width:100%}100%{left:100%;width:10%}}.neo_loader{position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999999999999999999999;background:#fff;height:100vh;width:100vw;display:flex;flex-direction:column;justify-content:center;align-items:center; padding: 40px;box-sizing: border-box;text-align: center;}.neo_loader .loaderline{width:400px;margin:40px;max-width:90vw}.loadername{color:#bbb;font-family:Helvetica,Arial,sans-serif;margin:10px auto 15px}.linear-activity.gray{background-color:#eaeaea}.gray .indeterminate:after,.gray .indeterminate:before{background-color:#bbb}.neo_megacontainer{display:none}</style> 2415 2416 <img style=" height: 250px; width: auto; object-fit: contain; max-width: 100%;" src="https://files-p.pxsol.com/13307/company/library/user/5126930364ea8a9a40b647af1c2cabae351ef5e772.png" alt="San Remo Hoteles"> 2417 <div class="loadername"> 2418 San Remo Hoteles </div> 2419 <div class="loaderline"> 2420 <div class="linear-activity gray"> 2421 <div class="indeterminate"></div> 2422 </div> 2423 </div> 2424</div> 2425 2426 2427 2428 <div class="main_wraper_top"> 2429 2430 2431<div class="neo_template_container neo_template_ computer"> 2432 2433 2434<div id='amerian_supratop' class="amerian_top_container diplo_top_container flex flex-m-center" ><div id='diploTopContainer' class="flex flex-s-center amerian_main_top" ><a class="diplo_top_logo padding-5 padding-left-20 fullh" href="/en/" target=""> <img loading="lazy" class="diplo_top_logo_img fullh" lazy-src="https://files-p.pxsol.com/13307/company/library/user/5126930364ea8a9a40b647af1c2cabae351ef5e772.png" alt="HSR TERMAS" > </image></a><div id='diploMenuTop' class="amerian_menu_container flex-column flex-grow fullh padding-left-20 padding-right-20" ><div class="amerian_supramenu flex flex-s-center padding-20" ><div class='diplo_idiomas fullh' ><div class='pxModule_include ModuloWrap_idiomas '><div id="top_menu"> 2435 2436 <div id="contenedor_top"> 2437 <div id="menu_div_top"> 2438 <div class="nav_top flex flex-s-center"> 2439 2440 2441 <div class="selectidiomaymoneda"> 2442 <div class="currency_module_container" > 2443 <input type="hidden" id='ModuloIdioma_DefaultCurrency' value="ARS"> 2444 <input type="hidden" id="ModuloIdioma_BookingEngineVersion" value="8"> 2445 <div class="texto_idiomaymoneda"> 2446 Currency - </div> 2447 <div id="deploymoneda" class='cursornormal';> 2448 <div class="monedaactual hidden" cur="ARS"> 2449 ARS $ </div> 2450 </div> 2451 </div> 2452 <div class="texto_idiomaymoneda"> 2453 Language - </div> 2454 2455 2456 <div id="deployidioma" class="deployidioma"> 2457 <img alt="flag" style="object-fit: contain;" class="bandera" src="/css/images/bnd_en.png" height="24px" width="24px" /> 2458 </div> 2459 2460 2461 <div id="idiomasshow" class="idiomasshow"> 2462 <div class="titlemenujs"> 2463 Choose the language of your preference </div> 2464 <div class="idiomaitem"> 2465 <a class="idiomalink" lng="es" href="/list.html?mkt_active=true&Pid=24471&origin=marketplace_homepage&lng=es" ignorar-vista="true" style="width: 100%;"> 2466 <img class="bandera" style="object-fit: contain;" src="/css/images/bnd_es.png" height="20" width="20" alt="es" /> 2467 <span class="nameidioma"> 2468 Español </span> 2469 </a> 2470 </div> 2471 <div class="idiomaitem"> 2472 <a class="idiomalink" lng="pt" href="/pt/list.html?mkt_active=true&Pid=24471&origin=marketplace_homepage&lng=pt" ignorar-vista="true" style="width: 100%;"> 2473 <img class="bandera" style="object-fit: contain;" src="/css/images/bnd_pt.png" height="20" width="20" alt="pt" /> 2474 <span class="nameidioma"> 2475 Português </span> 2476 </a> 2477 </div> 2478 <div class="idiomaitem"> 2479 <a class="idiomalink" lng="fr" href="/fr/list.html?mkt_active=true&Pid=24471&origin=marketplace_homepage&lng=fr" ignorar-vista="true" style="width: 100%;"> 2480 <img class="bandera" style="object-fit: contain;" src="/css/images/bnd_fr.png" height="20" width="20" alt="fr" /> 2481 <span class="nameidioma"> 2482 Français </span> 2483 </a> 2484 </div> 2485 </div>
2486 </div> 2487 2488 2489 <a href="https://app.reservadirecto.com/auth_web?return_url=https%3A%2F%2Fwww.sanremohoteles.com%2Fauthorize.php%3Fredirect_url%3Dhttps%253A%252F%252Fwww.sanremohoteles.com%252Flist.html%253Fmkt_active%253Dtrue%2526Pid%253D24471%2526origin%253Dmarketplace_homepage&origin_pos=Sanremohoteles" ignorar-vista="true" class="btn_sesion cta_webcheckin_px margin-sm-right-10" style="text-decoration: none"> 2490 <i class="mdi mdi-account" aria-hidden="true"></i> 2491 <span> 2492 Log in </span> 2493 </a> 2494 2495
2495<script src="https://accounts.google.com/gsi/intermediate"></script>
2495 2496
2496<script src="https://accounts.google.com/gsi/intermediatesupport"></script>
2496 2497 2498 <div id="g_id_intermediate_iframe" data-src="https://eva.reservadirecto.com/core/includes/google_one_tap_intermediate_iframe.php?origin=https%3A%2F%2Fwww.sanremohoteles.com&redirect_url=https%3A%2F%2Fwww.sanremohoteles.com%2Flist.html%3Fmkt_active%3Dtrue%26Pid%3D24471%26origin%3Dmarketplace_homepage&pos=Sanremohoteles"> 2499 </div> 2500 2501 <style> 2502 .neo_login_incentive { 2503 position: fixed; 2504 top: 10rem; 2505 right: calc(1rem + 10px); 2506 width: 375px; 2507 text-align: left; 2508 z-index: 1; 2509 } 2510 2511 .neo_login_incentive>div { 2512 margin: 0; 2513 border-radius: 10px; 2514 } 2515 2516 iframe#onetap_google_intermediate_iframe, 2517 #credential_picker_container, 2518 #credential_picker_container iframe, 2519 iframe[src*="google_one_tap_intermediate_iframe"] { 2520 top: 18.5rem !important; 2521 right: 1rem !important; 2522 } 2523 2524 @media only screen and (max-width: 468px) { 2525 .neo_login_incentive { 2526 display: none !important; 2527 bottom: 0rem; 2528 top: initial; 2529 left: 0; 2530 right: 0; 2531 position: fixed; 2532 width: 100%; 2533 text-transform: initial; 2534 letter-spacing: normal; 2535 font-weight: initial; 2536 } 2537 2538 iframe#onetap_google_intermediate_iframe, 2539 #credential_picker_container, 2540 #credential_picker_container iframe, 2541 iframe[src*="google_one_tap_intermediate_iframe"] { 2542 top: initial !important; 2543 bottom: 5rem !important; 2544 right: initial !important; 2545 z-index: 1; 2546 } 2547 } 2548 </style> 2549
2549<script> 2550 (function() { 2551 var repositioned = false; 2552 var attempts = 0; 2553 var timer = setInterval(function() { 2554 if (repositioned || ++attempts > 60) { clearInterval(timer); return; } 2555 var el = document.getElementById('credential_picker_container') 2556 || document.querySelector('iframe[src*="google_one_tap_intermediate_iframe"]') 2557 || document.getElementById('onetap_google_intermediate_iframe'); 2558 if (el) { 2559 var target = (el.tagName === 'IFRAME' && el.parentElement && el.parentElement.style.position === 'fixed') ? el.parentElement : el; 2560 var isMobile = window.innerWidth <= 468; 2561 if (isMobile) { 2562 target.style.setProperty('top', 'initial', 'important'); 2563 target.style.setProperty('bottom', '5rem', 'important'); 2564 target.style.setProperty('right', 'initial', 'important'); 2565 } else { 2566 target.style.setProperty('top', '18.5rem', 'important'); 2567 target.style.setProperty('right', '1rem', 'important'); 2568 } 2569 repositioned = true; 2570 clearInterval(timer); 2571 } 2572 }, 100); 2573 })(); 2574 </script>
2574 2575 2576 </div> 2577 </div> 2578 </div> 2579 2580</div> 2581</div></div></div></div><div class="diplo_cta padding-20 toggle_search fullh heightplus1" style='display: none'><i class='mdi mdi-room-service-outline margin-right-10 fontsize-20px' ></i><div class="" >Reserve now</div></div></div></div> 2582 2583</div> 2584 2585 </div> 2586 2587 2588 2589 <noscript>This search engine requires Javascript enabled to function. Please update your browser or use the latest version of Chrome or Firefox for a better browsing experience</noscript> 2590 2591 <div class="neo_megacontainer mktplace_contentblock extend padding-sm-top-80"> 2592 2593 <div style="display:none" id="loaderText"> 2594 Looking for the best prices for your stay</div> 2595
2596<script> 2597 function IncluirModuloViaJS() { 2598 2599 window.apiUrl = window.apiUrl ? window.apiUrl : "https://api-1-eb-web.pxsol.io/"; 2600 window.api2Url = window.api2Url ? window.api2Url : "https://api-2-eb.pxsol.io/api/"; 2601 window.publicApiUrl = "https://gateway-prod.pxsol.com/"; 2602 var loaderText = $("#loaderText").length ? $("#loaderText").text() + "..." : "Cargando..."; 2603 var urlParams = new URLSearchParams(window.location.search); 2604 var EmailTk = Email ? Email : "NN"; 2605 var userPar = urlParams.get("UserID") ? "&UserID=" + urlParams.get("UserID") : ""; 2606 var userTkPar = urlParams.get("Token") ? "&Token=" + urlParams.get("Token") : ""; 2607 var tagPar = urlParams.get("tag") ? urlParams.get("tag") : "NN"; 2608 var ListIDPar = urlParams.get("ListID") ? "&ListID=" + urlParams.get("ListID") : ""; 2609 var LocationPar = urlParams.get("Location") ? "&Location=" + encodeURIComponent(urlParams.get("Location")) : ""; 2610 var cartOriginPar = urlParams.get("origin") ? "&origin=" + urlParams.get("origin") : ""; 2611 var SearchIDPar = urlParams.get("SearchID") ? "&SearchID=" + urlParams.get("SearchID") : ""; 2612 var StartPar = urlParams.get("Start") ? "&Start=" + urlParams.get("Start") : ""; 2613 var EndPar = urlParams.get("End") ? "&End=" + urlParams.get("End") : ""; 2614 var GroupsPar = urlParams.get("Groups") ? "&Groups=" + urlParams.get("Groups") : ""; 2615 var ProductIDPar = urlParams.get("ProductID") ? "&ProductID=" + urlParams.get("ProductID") : ""; 2616 const Pid = urlParams.get("Pid") ? "&Pid=" + urlParams.get("Pid") : ""; 2617 tagPar = tagPar == "NN" ? urlParams.get("Tag") : tagPar; 2618 tagPar = tagPar ? tagPar : ""; 2619 tagPar = tagPar ? "&tag=" + tagPar : ''; 2620 2621 var urlCart = '/neo_marketplace_lp/cart_body.php?pos=' + Json_Pos.Pos + SearchIDPar + userPar + userTkPar + tagPar + LocationPar + ListIDPar + cartOriginPar + ProductIDPar + "&cur=" + CurrencyTk + "&lng=" + LngTk + "&Email=" + EmailTk + "&search=OK" + Pid; 2622 2623 if (!SearchIDPar) { 2624 var august_date = new Date('2020-08-01'); 2625 var d = new Date(); 2626 if (d.getTime() < august_date.getTime()) { 2627 d = august_date; 2628 } 2629 StartPar = !StartPar ? "&Start=" + d.toISOString().split('T')[0] : StartPar; 2630 d.setDate(d.getDate() + 1); 2631 EndPar = !EndPar ? "&End=" + d.toISOString().split('T')[0] : EndPar; 2632 ListIDPar = ListIDPar ? ListIDPar : (Json_Pos.DefaultListID ? "&ListID=" + Json_Pos.DefaultListID : ""); 2633 var searchQueryString = '?pos=' + Json_Pos.Pos + StartPar + EndPar + GroupsPar + ListIDPar + LocationPar + tagPar + ProductIDPar + "&cur=" + CurrencyTk + "&lng=" + LngTk + "&Email=" + EmailTk + "&search=NoSearchID" + Pid; 2634 var urlCart = '/neo_marketplace_lp/cart_body.php' + searchQueryString; 2635 2636 history.pushState({ 2637 url: searchQueryString 2638 }, 2639 $('title').text().trim(), 2640 searchQueryString 2641 ); 2642 } 2643 console.log("URL CART INITIAL LOAD: " + urlCart); 2644 CargarModulo(urlCart, loaderText, ".container_cargarmodulo"); 2645 } 2646</script>
2646 2647 2648<div class="neo_full container_cargarmodulo"> 2649 2650</div> 2651 2652 2653 </div> 2654 2655
2655<script> 2656 var deferSlickExecution = true; 2657 var deferFontAwesome = true; 2658 var deferMDI = true; 2659 2660 function runPostJquery() { 2661 var jsMain = document.createElement('script'); 2662 jsMain.async = true; 2663 jsMain.src = "https://cdn.pxsol.com/neo_marketplace_lp/main_lp.js?filev=1778144039"; 2664 document.body.appendChild(jsMain); 2665 var jsDevice = document.createElement('script'); 2666 jsDevice.async = true; 2667 jsDevice.src = "https://cdn.pxsol.com/neo_marketplace_lp/computer.js"; 2668 document.body.appendChild(jsDevice); 2669 if (typeof initializeMDI !== "undefined") { 2670 if (typeof initializeMDI === "function") { 2671 initializeMDI(); 2672 } 2673 } 2674 if (typeof FnLoadingOFF !== "undefined") { 2675 if (typeof FnLoadingOFF === "function") { 2676 FnLoadingOFF(); 2677 } 2678 } 2679 } 2680 </script>
2680 2681 2682
2682<script src='/js_3.js?filev=1778144039&pos=Sanremohoteles&device=computer&posv=1788314973_2025-08-29T20:43:48.000Z' async defer onload="runPostJquery()"></script>
2682 2683 2684 <div class="main_wraper_footer" id="main_wraper_footer"> 2685 2686 2687<div class="neo_template_container neo_template_ computer"> 2688 2689 2690<div class="diplo_footercontainer full flex flex-wrap padding-20 padding-top-60 padding-bottom-40" ><div class="xs-100 lg-10" ></div><div class="xs-100 lg-50 flex flex-column" > <img loading="lazy" class="amerian_footerimg margin-top-10 margin-bottom-30" lazy-src="https://files-p.pxsol.com/13307/company/library/user/5126930364ea8a9a40b647af1c2cabae351ef5e772.png" alt="San Remo Hoteles" > </image><div class="fontsize-1em diplo_white padding-bottom-30" ><div class="diplo_footerinfo flex-column" ><div class="margin-bottom-30 txtUpper txtWhite amerian_heading" >Reservation center</div><a class="margin-bottom-5 flex flex-s-center txtUpper amerian_supramenu_item nopadding fontsize-20px" href="tel:8102208880" target=""><div class="" >8102208880</div></a><a class="margin-bottom-5 flex flex-s-center txtUpper amerian_supramenu_item nopadding fontsize-20px" href="tel:5491127216237" target=""><div class="" >5491127216237</div></a></div></div></div><div class="xs-100 lg-30" ><div class="diplo_social flex flex-m-end flex-s-center padding-bottom-20 padding-right-20" ><div class='' > 2691 <div class="neo_template_block neo_neo_social_links pxModule_include ModuloWrap_neo_social_links" id="neo_social_links" > 2692 2693 2694<div class="neo_social_links_container social_block"> 2695 <ul class="neo_links_list"> 2696 <li class="facebook_page"> 2697 <a href="https://www.facebook.com/sanremohoteles" target="_blank"> 2698 <i class="mdi mdi-facebook"></i> 2699 </a> 2700 </li> 2701 <li class="instagram_page"> 2702 <a href="https://www.instagram.com/hotelessanremo" target="_blank"> 2703 <i class="mdi mdi-instagram"></i> 2704 </a> 2705 </li> 2706 <li class="linkedin_page"> 2707 <a href="https://www.linkedin.com/company/101938064" target="_blank"> 2708 <i class="mdi mdi-linkedin"></i> 2709 </a> 2710 </li> 2711 </ul> 2712</div> 2713 <div class="wsp_floating"> 2714 <a href="https://api.whatsapp.com/send?phone=5491127216237" target="_blank"> 2715 <i class="mdi mdi-whatsapp"></i> 2716 </a> 2717 </div> 2718 </div> 2719 </div><ul class='neo_links_list' ><li class='youtube_page' ><a class="" target='_blank' href="https://www.youtube.com/@sanremohoteles5949" target=""><i class='mdi mdi mdi-youtube' ></i></a></li></ul></div><div class="diplo_social flex flex-m-end flex-s-center padding-bottom-20 padding-right-20" ></div></div><div class="xs-100 lg-10" ></div></div> 2720 2721</div> 2722 2723 </div> 2724 2725 <div class="main_wraper_footer" id="main_wraper_footer"> 2726 <div id="footer_px" class="noprint footer_px_fix" style="max-width: 100vw; box-sizing: border-box; overflow-x: hidden;"> 2727 <div class="wraper_footerpx flex-column flex-center"> 2728 2729 2730 <div class="texto_footerpx"> 2731 By using this website, you agree to the use of cookies. <a target="_blank" rel="nofollow" href="/cookiespolicies.php?pos=Sanremohoteles" class="footer_link">Cookies policy</a> | <a target="_blank" rel="nofollow" href="/terms.php?pos=Sanremohoteles" class="footer_link">Terminos y Condiciones</a> | <a target="_blank" rel="nofollow" href="/policies.php?pos=Sanremohoteles" class="footer_link">Privacy Policy</a> | <span>Cache: 2732 2026-10-05 06:35:37 | </span><a href="" rel="nofollow" target="_blank">Textos</a> | <span class="framework_f">Framework: 3 | <span class="framework_f">Tag: <span class="tag_cookie">..</span> </span> </span> 2733 </div> 2734 2735 <div class="pxsol_link_f"> 2736 <a style="text-decoration: underline !important;" target="_blank" href="https://www.pxsol.com/pos_source=Sanremohoteles" class="footer_link">Sistema Hotelero</a> 2737 por <a href="/" target="_blank" class="footer_link footer_logo_svg" title="Sistema Hotelero"> 2738 <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2739<svg 2740 xmlns:dc="http://purl.org/dc/elements/1.1/" 2741 xmlns:cc="http://creativecommons.org/ns#" 2742 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 2743 xmlns:svg="http://www.w3.org/2000/svg" 2744 xmlns="http://www.w3.org/2000/svg" 2745 viewBox="0 0 752.42535 358.37866" 2746 height="50px" 2747 width="23px" 2748 xml:space="preserve" 2749 id="svg2" 2750 version="1.1"><metadata 2751 id="metadata8"><rdf:RDF><cc:Work 2752 rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type 2753 rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs 2754 id="defs6"><clipPath 2755 id="clipPath18" 2756 clipPathUnits="userSpaceOnUse"><path 2757 id="path16" 2758 d="M 0,268.784 H 564.319 V 0 H 0 Z" /></clipPath></defs><g 2759 transform="matrix(1.3333333,0,0,-1.3333333,0,358.37867)" 2760 id="g10"><g 2761 id="g12"><g 2762 clip-path="url(#clipPath18)" 2763 id="g14"><g 2764 transform="translate(23.3438,154.8828)" 2765 id="g20"><path 2766 id="path22" 2767 style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" 2768 d="m 0,0 h 12.405 c 11.978,0 20.768,1.891 26.369,5.674 5.601,3.781 8.4,9.691 8.4,17.728 0,7.447 -2.507,13.002 -7.521,16.666 -5.013,3.664 -12.828,5.496 -23.44,5.496 H 0 Z m 71.202,24.112 c 0,-13.534 -4.884,-23.905 -14.65,-31.115 -9.768,-7.21 -23.67,-10.814 -41.707,-10.814 H 0 V -66.309 H -23.344 V 63.294 h 41.118 c 17.841,0 31.207,-3.311 40.097,-9.929 8.886,-6.619 13.331,-16.37 13.331,-29.253" /></g><g 2769 transform="translate(325.4336,123.7686)" 2770 id="g24"><path 2771 id="path26" 2772 style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
2773 d="m 0,0 c 0,-11.524 -4.59,-20.565 -13.773,-27.126 -9.179,-6.561 -21.846,-9.84 -37.994,-9.84 -16.149,0 -29.367,2.274 -39.653,6.824 v 20.036 c 6.509,-2.78 13.43,-4.964 20.754,-6.56 7.327,-1.596 14.147,-2.392 20.462,-2.392 9.246,0 16.068,1.594 20.462,4.785 4.396,3.191 6.595,7.475 6.595,12.853 0,4.847 -2.02,8.956 -6.057,12.323 -4.039,3.369 -12.372,7.358 -25.005,11.968 -13.023,4.787 -22.204,10.252 -27.542,16.4 -5.341,6.145 -8.01,13.532 -8.01,22.161 0,10.815 4.233,19.324 12.698,25.53 8.463,6.205 19.827,9.308 34.087,9.308 13.675,0 27.284,-2.719 40.829,-8.156 L -9.572,70.829 c -12.697,4.844 -24.028,7.269 -33.99,7.269 -7.555,0 -13.285,-1.494 -17.19,-4.477 -3.906,-2.985 -5.86,-6.93 -5.86,-11.834 0,-3.369 0.781,-6.25 2.344,-8.644 1.563,-2.393 4.134,-4.653 7.716,-6.782 3.58,-2.127 10.026,-4.935 19.339,-8.421 10.484,-3.959 18.166,-7.654 23.05,-11.08 4.884,-3.429 8.464,-7.3 10.745,-11.613 C -1.14,10.933 0,5.85 0,0" /></g><g 2774 transform="translate(355.8486,153.5532)" 2775 id="g28"><path 2776 id="path30" 2777 style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" 2778 d="m 0,0 c 0,-15.956 3.547,-28.057 10.646,-36.299 7.097,-8.245 17.614,-12.369 31.548,-12.369 13.87,0 24.335,4.08 31.401,12.235 7.064,8.155 10.598,20.3 10.598,36.433 0,15.897 -3.501,27.953 -10.5,36.168 C 66.693,44.382 56.26,48.49 42.39,48.49 28.391,48.49 17.826,44.382 10.694,36.168 3.564,27.953 0,15.897 0,0 m 109.001,0 c 0,-21.097 -5.811,-37.498 -17.434,-49.198 C 79.946,-60.901 63.486,-66.75 42.194,-66.75 c -21.553,0 -38.108,5.806 -49.667,17.418 -11.558,11.613 -17.334,28.116 -17.334,49.509 0,21.394 5.81,37.823 17.433,49.288 11.622,11.464 28.21,17.198 49.764,17.198 21.227,0 37.635,-5.822 49.227,-17.464 C 103.205,37.556 109.001,21.156 109.001,0" /></g><g 2779 transform="translate(480.5166,88.5742)" 2780 id="g32"><path 2781 id="path34" 2782 style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none" 2783 d="M 0,0 V 129.603 H 23.342 V 18.175 H 83.803 V 0 Z" /></g><g 2784 transform="translate(95.5459,218.4404)" 2785 id="g36"><path 2786 id="path38" 2787 style="fill:#ee2735;fill-opacity:1;fill-rule:nonzero;stroke:none" 2788 d="m 0,0 42.967,-58.803 -68.085,-93.3 30.263,-9.074 56.235,77.141 87.961,-120.538 46.28,-13.866 L 79.583,-59.091 159.441,50.344 112.198,36.084 60.982,-33.892 29.65,9.046 Z" /></g></g></g></g></svg> </a> 2789 </div> 2790 2791 </div> 2792</div> 2793 2794
2795<script> 2796 function loadPxChatWidget() { 2797 const posToGetApiKey = 'Sanremohoteles'; 2798 const timestamp = '1788314973_2025-08-29T20:43:48.000Z'; 2799 const urlToGetApiKey = `https://px-chat-widget-prod.pxsol.com/company-widget/apiKey/${posToGetApiKey}?v=${timestamp}`; 2800 2801 fetch(urlToGetApiKey, { 2802 method: 'GET', 2803 headers: { 2804 'Content-Type': 'application/json', 2805 } 2806 }) 2807 .then(response => { 2808 if (!response.ok) { 2809 throw new Error(`HTTP error! status: ${response.status}`); 2810 } 2811 return response.json(); 2812 }) 2813 .then(data => { 2814 const internalKey = data?.data?.api_key; 2815 if (!internalKey) { 2816 throw new Error('API key not found in response'); 2817 } 2818 2819 const script = document.createElement('script'); 2820 script.src = `https://cdn.pxsol.com/dist/chat-widget.min.js?k=${internalKey}&v=${timestamp}`; 2821 script.async = true; 2822 script.onload = () => { 2823 console.log('PxChat widget loaded successfully'); 2824 }; 2825 script.onerror = (error) => { 2826 console.error('Failed to load PxChat widget:', error); 2827 }; 2828 document.body.appendChild(script); 2829 }) 2830 .catch(error => { 2831 console.error('PxChat widget initialization failed:', error.message); 2832 }); 2833 } 2834 document.addEventListener("DOMContentLoaded", loadPxChatWidget); 2835 </script>
2835 2836 2837<style> 2838 /* El botón flotante de WhatsApp (.wsp_floating) y el launcher del widget de chat (pxChat) 2839 se ubican ambos en la esquina inferior derecha y chocan entre sÃ. Cuando el chat está 2840 activo/montado, ocultamos el botón de WhatsApp para evitar la superposición. */ 2841 body.px-chat-widget-active .wsp_floating { display: none !important; } 2842</style>
2843<script> 2844 (function () { 2845 // El widget pxChat monta su launcher/panel bajo #px-widget-root (clase .widget-launcher-button). 2846 function chatWidgetPresent() { 2847 return !!( 2848 document.getElementById('px-widget-root') || 2849 document.querySelector('.widget-launcher-button, [class*="widget-launcher"]') 2850 ); 2851 } 2852 var applied = false; 2853 var observer = null; 2854 var fallbackTimer = null; 2855 function apply() { 2856 if (applied || !document.body || !chatWidgetPresent()) return; 2857 document.body.classList.add('px-chat-widget-active'); 2858 applied = true; 2859 if (observer) observer.disconnect(); 2860 if (fallbackTimer) clearTimeout(fallbackTimer); 2861 } 2862 function start() { 2863 apply(); 2864 if (applied || !('MutationObserver' in window) || !document.body) return; 2865 // El widget carga async (fetch de apiKey + inyección del
2865<script>), asà que observamos 2866 // el DOM hasta que aparezca. Si a los 20s no montó, dejamos de observar. 2867 observer = new MutationObserver(apply); 2868 observer.observe(document.body, { childList: true, subtree: true }); 2869 fallbackTimer = setTimeout(function () { 2870 if (observer) observer.disconnect(); 2871 }, 20000); 2872 } 2873 if (document.readyState === 'loading') { 2874 document.addEventListener('DOMContentLoaded', start); 2875 } else { 2876 start(); 2877 } 2878 })(); 2879</script>
2879 2880
2881<script src="https://cdn.pxsol.com/js/domready.js"></script>
vendor: 1 bytes, line 2881
2881
2882<script> 2883 var head = document.getElementsByTagName('head')[0]; 2884 2885 function initializeFontAwesome() { 2886 var link = document.createElement('link'); 2887 link.id = "fawesome"; 2888 link.rel = 'stylesheet'; 2889 link.type = 'text/css'; 2890 link.href = 'https://use.fontawesome.com/releases/v5.7.1/css/all.css'; 2891 head.appendChild(link); 2892 } 2893 2894 function initializeMDI() { 2895 var link = document.createElement('link'); 2896 link.id = "fawesome"; 2897 link.rel = 'stylesheet'; 2898 link.type = 'text/css'; 2899 link.href = '/css/materialdesignicons.min.css'; 2900 head.appendChild(link); 2901 } 2902 2903 function initializeToast() { 2904 // check if jquery is loaded 2905 if (typeof jQuery == 'function') { 2906 let link = document.createElement('link'); 2907 link.id = "jqToastCss"; 2908 link.rel = 'stylesheet'; 2909 link.type = 'text/css'; 2910 link.href = '/includes/toast/jquery.toast.min.css'; 2911 head.appendChild(link); 2912 let script = document.createElement('script'); 2913 script.id = "jqToastJs"; 2914 script.type = 'text/javascript'; 2915 script.defer = true; 2916 script.async = true; 2917 script.src = '/includes/toast/jquery.toast.min.js'; 2918 head.appendChild(script); 2919 } else { 2920 console.log('jQuery is not loaded. Toast could not be initialized.'); 2921 } 2922 } 2923 2924 DomReady.ready(function() { 2925 var link = document.createElement('link'); 2926 link.id = "siteCss"; 2927 link.rel = 'stylesheet'; 2928 link.type = 'text/css'; 2929 2930 link.href = 2931 '/css_3.css?pos=Sanremohoteles&device=computer&posv=1788314973_2025-08-29T20:43:48.000Z_2025-08-29T20:43:48.000Z&filev=1782498034'; 2932 head.appendChild(link); 2933 2934 if (typeof deferFontAwesome !== "undefined") { 2935 if (!deferFontAwesome) { 2936 initializeFontAwesome(); 2937 } 2938 } else { 2939 initializeFontAwesome(); 2940 } 2941 2942 if (typeof deferMDI !== "undefined") { 2943 if (!deferMDI) { 2944 initializeMDI(); 2945 } 2946 } else { 2947 initializeMDI(); 2948 } 2949 2950 if (typeof deferToast !== "undefined") { 2951 if (!deferToast) { 2952 initializeToast(); 2953 } 2954 } else { 2955 initializeToast(); 2956 } 2957 2958 }); 2959</script>
vendor: 1 bytes, line 2959
2959
2960<script> 2961 (function(){ 2962 console.log('%c[pxChat] ð Chat tracking script loaded (Chromium optimized)', 'color: #4CAF50; font-weight: bold; font-size: 14px;'); 2963 window.dataLayer = window.dataLayer || []; 2964 function generateId(prefix){ 2965 const rnd = ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => 2966 (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 2967 ); 2968 return (prefix || 'id_') + rnd; 2969 } 2970 function getPxContext(){ 2971 try { return window.pxContext || {}; } catch(e){ return {}; } 2972 } 2973 function getSessionId(){ 2974 const ctx = getPxContext(); 2975 if (ctx.session_id) return ctx.session_id; 2976 let sid = sessionStorage.getItem('px_session_id'); 2977 if (!sid){ sid = generateId('s_'); sessionStorage.setItem('px_session_id', sid); } 2978 return sid; 2979 } 2980 function getChatSessionId(){ 2981 let cid = sessionStorage.getItem('px_chat_session_id'); 2982 if (!cid){ cid = generateId('c_'); sessionStorage.setItem('px_chat_session_id', cid); } 2983 return cid; 2984 } 2985 function basePayload(){ 2986 const ctx = getPxContext(); 2987 const pos = (ctx.pos || 'Sanremohoteles') || null; 2988 const hotelId = (ctx.product_id || '24471') || null; 2989 return { 2990 session_id: getSessionId(), 2991 search_id: ctx.search_id || ctx.SearchID || null, 2992 pos: pos, 2993 post: pos, 2994 hotel_id: hotelId, 2995 page_url: location.href 2996 }; 2997 } 2998 function pushEvent(name, extra){ 2999 const params = Object.assign({}, basePayload(), extra || {}); 3000 // Dedupe: evita repetidos en ±1s por sesión/mensaje 3001 try { 3002 const keyId = params.chat_session_id || params.chat_id || ''; 3003 const key = name + '|' + String(keyId); 3004 const now = Date.now(); 3005 window.__px_evt_dedupe = window.__px_evt_dedupe || {}; 3006 const last = window.__px_evt_dedupe[key] || 0; 3007 if (now - last < 1000) { 3008 return; 3009 } 3010 window.__px_evt_dedupe[key] = now; 3011 } catch(_e){ 3012 console.error('[pxChat] Error in pushEvent dedupe:', _e); 3013 } 3014 // Evitar duplicados: si pxTrack existe, delegamos y NO empujamos a dataLayer aquà 3015 if (typeof window.pxTrack === 'function') { 3016 try { 3017 console.log('[pxChat] ð¤ Calling pxTrack for', name, 'with params:', params); 3018 window.pxTrack(name, params); 3019 console.log('%c[pxChat] â ' + name + ' event dispatched successfully', 'color: #4CAF50; font-weight: bold;'); 3020 } catch(e){ 3021 console.error('[pxChat] â Error calling window.pxTrack for', name, ':', e); 3022 } 3023 return; 3024 } 3025 const payload = Object.assign({ event: name }, params); 3026 console.log('[pxChat] ð¤ Pushing to dataLayer:', payload); 3027 window.dataLayer.push(payload); 3028 console.log('%c[pxChat] â ' + name + ' event pushed to dataLayer', 'color: #4CAF50; font-weight: bold;'); 3029 } 3030 let chatOpenTsMs = null; 3031 let chatIsOpen = false; 3032 let lastOpenPushMs = 0; 3033 let closeGuardTimer = null; 3034 let lastStateChangeMs = 0; 3035 let lastCloseCallMs = 0; // Guard para prevenir múltiples llamadas rápidas a handleChatClose 3036 let lastOpenCallMs = 0; // Guard para prevenir múltiples llamadas rápidas a handleChatOpen 3037 let MIN_CHAT_DURATION_MS = 3000; // MÃnimo 3 segundos entre chat_open y chat_close (previene falsos positivos en Chrome) 3038 let OPEN_DEBOUNCE_MS = 300; // Debounce para chat_open (300ms - solo para múltiples llamadas rápidas) 3039 let CLOSE_DEBOUNCE_MS = 400; // Debounce para chat_close (400ms - solo para múltiples llamadas rápidas) 3040 let pendingOpenTimer = null; // Timer para debounce de open 3041 let pendingCloseTimer = null; // Timer para debounce de close 3042 let openCallCount = 0; // Contador de llamadas rápidas a open 3043 let closeCallCount = 0; // Contador de llamadas rápidas a close 3044 function syncChatState(){ 3045 try { 3046 // Guard mÃnimo: no sincronizar si acabamos de cambiar estado hace menos de 200ms 3047 const now = Date.now(); 3048 if (now - lastStateChangeMs < 200) { 3049 console.log('[pxChat] syncChatState throttled (' + (now - lastStateChangeMs) + 'ms ago)'); 3050 return; 3051 } 3052 // Priorizar estado del launcher: clase --open 3053 // Buscar el botón con múltiples selectores 3054 const btn = document.querySelector('.widget-launcher-button') || 3055 document.querySelector('[class*="widget-launcher"]') || 3056 document.querySelector('[class*="launcher-button"]'); 3057 if (btn && btn.classList) { 3058 const isOpenBtn = btn.classList.contains('widget-launcher-button--open') || 3059 btn.classList.toString().indexOf('--open') !== -1; 3060 console.log('[pxChat] syncChatState: button state check:', { 3061 isOpenBtn: isOpenBtn, 3062 chatIsOpen: chatIsOpen,
3063 buttonClasses: btn.className 3064 }); 3065 if (isOpenBtn && !chatIsOpen) { 3066 console.log('[pxChat] syncChatState: button is open but chatIsOpen is false, calling handleChatOpen'); 3067 lastStateChangeMs = now; 3068 return handleChatOpen(); 3069 } 3070 if (!isOpenBtn && chatIsOpen) { 3071 // Guard mÃnimo: no cerrar si el chat se abrió hace menos de 3 segundos 3072 const startMs = chatOpenTsMs || lastOpenPushMs || null; 3073 if (startMs && (now - startMs < 3000)) { 3074 console.log('[pxChat] syncChatState: chat_close blocked - chat opened too recently (' + Math.round((now - startMs) / 1000) + 's ago, minimum 3s)'); 3075 return; 3076 } 3077 // CRÃTICO: Esperar y verificar nuevamente que el botón realmente está cerrado 3078 // Esto previene disparos prematuros cuando el DOM aún se está actualizando 3079 setTimeout(function() { 3080 const btnCheck = document.querySelector('.widget-launcher-button') || 3081 document.querySelector('[class*="widget-launcher"]') || 3082 document.querySelector('[class*="launcher-button"]'); 3083 const isStillOpen = btnCheck && btnCheck.classList && ( 3084 btnCheck.classList.contains('widget-launcher-button--open') || 3085 btnCheck.classList.toString().indexOf('--open') !== -1 3086 ); 3087 const ifrCheck = findChatIframe(); 3088 const isIframeVisible = isElementVisible(ifrCheck); 3089 3090 console.log('[pxChat] syncChatState: delayed verification after button close:', { 3091 isStillOpen: isStillOpen, 3092 isIframeVisible: isIframeVisible, 3093 chatIsOpen: chatIsOpen 3094 }); 3095 3096 if (isStillOpen) { 3097 console.log('[pxChat] syncChatState: chat_close CANCELLED - button still shows as open after delay'); 3098 return; 3099 } 3100 if (isIframeVisible) { 3101 console.log('[pxChat] syncChatState: chat_close CANCELLED - iframe still visible after delay'); 3102 return; 3103 } 3104 if (!chatIsOpen) { 3105 console.log('[pxChat] syncChatState: chat_close CANCELLED - chatIsOpen is already false'); 3106 return; 3107 } 3108 3109 console.log('[pxChat] syncChatState: button confirmed closed after delay, calling handleChatClose'); 3110 lastStateChangeMs = Date.now(); 3111 handleChatClose(); 3112 }, 600); // Esperar 600ms para confirmar que el DOM se actualizó completamente 3113 return; 3114 } 3115 } 3116 // CRÃTICO: NO usar iframe como fallback si hay un botón disponible 3117 // El iframe puede no ser visible incluso cuando el chat está abierto 3118 // Solo confiar en el botón launcher para determinar el estado 3119 if (!btn) { 3120 // Solo si NO hay botón, intentar buscar el botón de nuevo con un selector más amplio 3121 const btnRetry = document.querySelector('.widget-launcher-button, [class*="widget-launcher"], [class*="launcher-button"]'); 3122 if (btnRetry && btnRetry.classList) { 3123 const isOpenBtnRetry = btnRetry.classList.contains('widget-launcher-button--open') || 3124 btnRetry.classList.toString().indexOf('--open') !== -1; 3125 console.log('[pxChat] syncChatState: retry button found:', { 3126 isOpenBtnRetry: isOpenBtnRetry, 3127 chatIsOpen: chatIsOpen, 3128 buttonClasses: btnRetry.className 3129 }); 3130 if (isOpenBtnRetry && !chatIsOpen) { 3131 console.log('[pxChat] syncChatState: retry button is open but chatIsOpen is false, calling handleChatOpen'); 3132 lastStateChangeMs = now; 3133 return handleChatOpen(); 3134 } 3135 if (!isOpenBtnRetry && chatIsOpen) { 3136 // Guard mÃnimo: no cerrar si el chat se abrió hace menos de 3 segundos 3137 const startMs = chatOpenTsMs || lastOpenPushMs || null; 3138 if (startMs && (now - startMs < 3000)) { 3139 console.log('[pxChat] syncChatState: chat_close blocked - chat opened too recently (' + Math.round((now - startMs) / 1000) + 's ago, minimum 3s)'); 3140 return; 3141 } 3142 // Verificar nuevamente antes de cerrar 3143 setTimeout(function() { 3144 const btnCheck = document.querySelector('.widget-launcher-button, [class*="widget-launcher"], [class*="launcher-button"]'); 3145 const isStillOpen = btnCheck && (btnCheck.classList.contains('widget-launcher-button--open') || 3146 btnCheck.classList.toString().indexOf('--open') !== -1); 3147 3148 if (isStillOpen || !chatIsOpen) { 3149 console.log('[pxChat] syncChatState: chat_close CANCELLED - button still open or chatIsOpen is false'); 3150 return; 3151 } 3152 3153 console.log('[pxChat] syncChatState: retry button confirmed closed after delay, calling handleChatClose'); 3154 lastStateChangeMs = Date.now(); 3155 handleChatClose(); 3156 }, 600); 3157 return; 3158 } 3159 } 3160 3161 // ÃLTIMO RECURSO: Solo usar iframe si NO hay botón Y el chat está abierto 3162 // Y solo para abrir, NUNCA para cerrar (el iframe no es confiable para cerrar) 3163 const ifr = findChatIframe(); 3164 const visible = isElementVisible(ifr); 3165 console.log('[pxChat] syncChatState: iframe LAST RESORT check (only for opening):', { 3166 visible: visible, 3167 chatIsOpen: chatIsOpen, 3168 hasIframe: !!ifr 3169 }); 3170 // Solo usar iframe para detectar apertura, NUNCA para cerrar 3171 if (visible && !chatIsOpen) { 3172 console.log('[pxChat] syncChatState: iframe visible but chatIsOpen is false, calling handleChatOpen'); 3173 lastStateChangeMs = now; 3174 return handleChatOpen(); 3175 } 3176 // NO usar iframe para cerrar - solo confiar en el botón 3177 if (!visible && chatIsOpen) { 3178 console.log('[pxChat] syncChatState: iframe not visible but chatIsOpen is true - IGNORING (iframe not reliable for closing)'); 3179 return; // NO hacer nada - solo el botón puede cerrar 3180 } 3181 } 3182 } catch(_e){ 3183 console.error('[pxChat] syncChatState error:', _e); 3184 } 3185 } 3186 function handleChatOpen(meta){ 3187 console.log('[pxChat] handleChatOpen called:', { chatIsOpen: chatIsOpen, meta: meta }); 3188 3189 // Si ya está abierto, no hacer nada 3190 if (chatIsOpen) { 3191 console.log('[pxChat] handleChatOpen skipped - chat already open'); 3192 return; 3193 } 3194 3195 const now = Date.now(); 3196
3197 // Guard mÃnimo: no abrir si se cerró hace menos de 500ms (solo para prevenir toggle extremo) 3198 if (lastCloseCallMs && (now - lastCloseCallMs < 500)) { 3199 console.log('[pxChat] handleChatOpen blocked - chat closed too recently (' + (now - lastCloseCallMs) + 'ms ago)'); 3200 return; 3201 } 3202 3203 // Throttling mÃnimo: solo prevenir llamadas duplicadas en menos de 200ms 3204 if (now - lastOpenCallMs < 200) { 3205 console.log('[pxChat] handleChatOpen throttled (too soon)'); 3206 return; 3207 } 3208 3209 lastOpenCallMs = now; 3210 3211 // Cancelar cualquier close pendiente 3212 if (pendingCloseTimer) { 3213 clearTimeout(pendingCloseTimer); 3214 pendingCloseTimer = null; 3215 console.log('[pxChat] handleChatOpen: cancelled pending close'); 3216 } 3217 3218 // Disparar inmediatamente (sin debounce para Chromium) 3219 console.log('[pxChat] handleChatOpen: calling doChatOpen immediately'); 3220 doChatOpen(meta); 3221 } 3222 3223 function doChatOpen(meta) { 3224 // Verificar estado del botón (prioridad máxima para Chromium) 3225 // Buscar el botón con múltiples selectores 3226 const btn = document.querySelector('.widget-launcher-button') || 3227 document.querySelector('[class*="widget-launcher"]') || 3228 document.querySelector('[class*="launcher-button"]'); 3229 const isActuallyOpen = btn && btn.classList && ( 3230 btn.classList.contains('widget-launcher-button--open') || 3231 btn.classList.toString().indexOf('--open') !== -1 3232 ); 3233 const ifr = findChatIframe(); 3234 const isIframeVisible = isElementVisible(ifr); 3235 3236 // Log detallado para debugging en Chromium 3237 console.log('[pxChat] doChatOpen called:', { 3238 isActuallyOpen: isActuallyOpen, 3239 isIframeVisible: isIframeVisible, 3240 chatIsOpen: chatIsOpen, 3241 hasButton: !!btn, 3242 buttonClasses: btn ? btn.className : 'no button' 3243 }); 3244 3245 // Si el botón existe y dice que está abierto, proceder 3246 // Si no hay botón pero el iframe es visible, también proceder 3247 if (!isActuallyOpen && !isIframeVisible && btn) { 3248 console.warn('[pxChat] doChatOpen: button exists but not showing as open, proceeding anyway'); 3249 } 3250 3251 // Guardar timestamp ANTES de cambiar el estado (importante para Chrome) 3252 const now = Date.now(); 3253 chatOpenTsMs = now; 3254 lastOpenPushMs = now; 3255 chatIsOpen = true; 3256 const chat_session_id = (meta && (meta.chat_session_id || meta.session_id)) || getChatSessionId(); 3257 3258 // Log para debugging 3259 console.log('[pxChat] ð FIRING chat_open event:', { 3260 chatOpenTsMs: chatOpenTsMs, 3261 lastOpenPushMs: lastOpenPushMs, 3262 timestamp: now, 3263 chat_session_id: chat_session_id 3264 }); 3265 pushEvent('chat_open', { chat_session_id }); 3266 } 3267 function handleChatMessage(meta){ 3268 const chat_id = meta && (meta.chat_id || meta.message_id) || null; 3269 const message_type = meta && (meta.message_type || meta.type) || null; 3270 const payload = { chat_id, message_type }; 3271 pushEvent('chat_message', payload); 3272 } 3273 function handleChatClose(meta){ 3274 console.log('[pxChat] handleChatClose called:', { chatIsOpen: chatIsOpen, meta: meta }); 3275 3276 // Si ya está cerrado, no hacer nada 3277 if (!chatIsOpen) { 3278 console.log('[pxChat] handleChatClose skipped - chat already closed'); 3279 return; 3280 } 3281 3282 const now = Date.now(); 3283 3284 // Guard crÃtico: prevenir que chat_close se dispare inmediatamente después de chat_open 3285 // Aumentado a 3 segundos para evitar disparos prematuros 3286 const startMs = chatOpenTsMs || lastOpenPushMs || null; 3287 if (startMs && (now - startMs < 3000)) { 3288 console.log('[pxChat] handleChatClose blocked - chat opened too recently (' + Math.round((now - startMs) / 1000) + 's ago, minimum 3s required)'); 3289 return; 3290 } 3291 3292 // Verificación adicional: asegurarse de que el botón realmente está cerrado 3293 // Buscar el botón con múltiples selectores 3294 const btn = document.querySelector('.widget-launcher-button') || 3295 document.querySelector('[class*="widget-launcher"]') || 3296 document.querySelector('[class*="launcher-button"]'); 3297 const isActuallyOpen = btn && btn.classList && ( 3298 btn.classList.contains('widget-launcher-button--open') || 3299 btn.classList.toString().indexOf('--open') !== -1 3300 ); 3301 if (isActuallyOpen) { 3302 console.log('[pxChat] handleChatClose blocked - button still shows as open'); 3303 return; 3304 } 3305 3306 // Throttling mÃnimo: solo prevenir llamadas duplicadas en menos de 200ms 3307 if (now - lastCloseCallMs < 200) { 3308 console.log('[pxChat] handleChatClose throttled (too soon)'); 3309 return; 3310 } 3311 3312 lastCloseCallMs = now; 3313 3314 // Cancelar cualquier open pendiente 3315 if (pendingOpenTimer) { 3316 clearTimeout(pendingOpenTimer); 3317 pendingOpenTimer = null; 3318 console.log('[pxChat] handleChatClose: cancelled pending open'); 3319 } 3320 3321 // Disparar inmediatamente (sin debounce para Chromium)
3322 console.log('[pxChat] handleChatClose: calling doChatClose immediately'); 3323 doChatClose(meta, startMs); 3324 } 3325 3326 function doChatClose(meta, startMs) { 3327 // Verificar estado del botón (prioridad máxima para Chromium) 3328 // Buscar el botón con múltiples selectores 3329 const btn = document.querySelector('.widget-launcher-button') || 3330 document.querySelector('[class*="widget-launcher"]') || 3331 document.querySelector('[class*="launcher-button"]'); 3332 const isActuallyOpen = btn && btn.classList && ( 3333 btn.classList.contains('widget-launcher-button--open') || 3334 btn.classList.toString().indexOf('--open') !== -1 3335 ); 3336 const ifr = findChatIframe(); 3337 const isIframeVisible = isElementVisible(ifr); 3338 3339 // Log detallado para debugging en Chromium 3340 console.log('[pxChat] doChatClose called:', { 3341 isActuallyOpen: isActuallyOpen, 3342 isIframeVisible: isIframeVisible, 3343 chatIsOpen: chatIsOpen, 3344 hasButton: !!btn, 3345 buttonClasses: btn ? btn.className : 'no button', 3346 startMs: startMs 3347 }); 3348 3349 // CRÃTICO: Solo proceder si el chat realmente está cerrado 3350 // Si hay un botón, confiar solo en el botón (no en el iframe) 3351 if (btn) { 3352 if (isActuallyOpen) { 3353 console.warn('[pxChat] doChatClose BLOCKED - button still shows as open'); 3354 return; 3355 } 3356 // Si hay botón y no está abierto, proceder (ignorar iframe) 3357 console.log('[pxChat] doChatClose: button exists and is closed, proceeding'); 3358 } else { 3359 // Solo si NO hay botón, verificar iframe 3360 if (isIframeVisible) { 3361 console.warn('[pxChat] doChatClose BLOCKED - iframe still visible (no button found)'); 3362 return; 3363 } 3364 console.log('[pxChat] doChatClose: no button found, iframe not visible, proceeding'); 3365 } 3366 3367 // Guardar timestamp ANTES de cambiar el estado (importante para Chrome) 3368 const now = Date.now(); 3369 const endMs = now; 3370 // Calcular duración antes de resetear variables 3371 const duration_sec = startMs ? Math.max(0, Math.round((endMs - startMs) / 1000)) : null; 3372 // Ahora sà cambiar el estado 3373 chatIsOpen = false; 3374 const chat_session_id = (meta && (meta.chat_session_id || meta.session_id)) || getChatSessionId(); 3375 3376 // Log para debugging 3377 console.log('[pxChat] ð FIRING chat_close event:', { 3378 startMs: startMs, 3379 endMs: endMs, 3380 duration_sec: duration_sec, 3381 chatOpenTsMs: chatOpenTsMs, 3382 lastOpenPushMs: lastOpenPushMs, 3383 calculated: endMs - startMs + 'ms', 3384 chat_session_id: chat_session_id 3385 }); 3386 pushEvent('chat_close', { chat_session_id, duration_sec }); 3387 // Resetear timestamp inmediatamente (ya calculamos la duración) 3388 chatOpenTsMs = null; 3389 lastOpenPushMs = 0; 3390 } 3391 function isElementVisible(el){ 3392 if (!el) return false; 3393 const cs = window.getComputedStyle ? window.getComputedStyle(el) : null; 3394 if (!cs || cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false; 3395 const r = el.getBoundingClientRect(); 3396 return !!(r.width && r.height); 3397 } 3398 function findChatIframe(){ 3399 try { 3400 const ifrs = document.querySelectorAll('iframe'); 3401 for (let i = 0; i < ifrs.length; i++){ 3402 const n = ifrs[i]; 3403 if (isChatIframe(n)) return n; 3404 } 3405 } catch(_e){} 3406 return null; 3407 } 3408 // Detect launcher button presence (instala watcher; no toggle directo) 3409 document.addEventListener('click', function(ev){ 3410 try { 3411 // Detectar click en el botón de cerrar del header 3412 let closeBtn = null; 3413 if (ev.composedPath) { 3414 const path = ev.composedPath(); 3415 for (let i = 0; i < path.length; i++) { 3416 const n = path[i]; 3417 if (n && n.classList && n.classList.contains && n.classList.contains('widget-header-close')) { 3418 closeBtn = n; 3419 break; 3420 } 3421 } 3422 } 3423 if (!closeBtn && ev.target && ev.target.closest) { 3424 closeBtn = ev.target.closest('.widget-header-close'); 3425 } 3426 3427 // Si se hizo click en el botón de cerrar, esperar a que el DOM se actualice 3428 if (closeBtn && chatIsOpen) { 3429 console.log('[pxChat] Close button clicked, waiting for DOM update...'); 3430 // Esperar más tiempo para que el widget actualice el estado del botón 3431 setTimeout(function() { 3432 const launcherBtn = document.querySelector('.widget-launcher-button'); 3433 const isStillOpen = launcherBtn && launcherBtn.classList && launcherBtn.classList.contains('widget-launcher-button--open'); 3434 console.log('[pxChat] After close button click, launcher state:', { 3435 isStillOpen: isStillOpen, 3436 chatIsOpen: chatIsOpen 3437 }); 3438 if (!isStillOpen && chatIsOpen) { 3439 console.log('[pxChat] Close button clicked and launcher is now closed, calling handleChatClose'); 3440 handleChatClose(); 3441 } 3442 }, 500); // Esperar 500ms para que el DOM se actualice 3443 return; 3444 } 3445 3446 // Detectar click en el botón launcher 3447 let btn = null; 3448 if (ev.composedPath) { 3449 const path = ev.composedPath(); 3450 for (let i = 0; i < path.length; i++) { 3451 const n = path[i]; 3452 if (n && n.classList && n.classList.contains && n.classList.contains('widget-launcher-button')) { btn = n; break; } 3453 } 3454 } 3455 if (!btn && ev.target && ev.target.closest) { 3456 btn = ev.target.closest('.widget-launcher-button'); 3457 } 3458 if (btn) { 3459 installButtonListener(btn); 3460 // Si el botón tiene --open, está abierto; si no, está cerrado 3461 const isOpen = btn.classList.contains('widget-launcher-button--open'); 3462 if (isOpen && !chatIsOpen) { 3463 // Chat se está abriendo 3464 console.log('[pxChat] Launcher button clicked (opening), calling handleChatOpen'); 3465 setTimeout(function() { handleChatOpen(); }, 300); 3466 } else if (!isOpen && chatIsOpen) { 3467 // Chat se está cerrando 3468 console.log('[pxChat] Launcher button clicked (closing), waiting for DOM update...'); 3469 setTimeout(function() { 3470 const launcherBtn = document.querySelector('.widget-launcher-button'); 3471 const isStillOpen = launcherBtn && launcherBtn.classList && launcherBtn.classList.contains('widget-launcher-button--open'); 3472 if (!isStillOpen && chatIsOpen) { 3473 console.log('[pxChat] Launcher button clicked and confirmed closed, calling handleChatClose'); 3474 handleChatClose(); 3475 } 3476 }, 500); // Esperar 500ms para confirmar que está cerrado 3477 } 3478 } 3479 } catch(_e){ 3480 console.error('[pxChat] Error in click listener:', _e); 3481 } 3482 }, true); 3483 // Keyboard open on launcher 3484 document.addEventListener('keydown', function(ev){ 3485 try { 3486 if (ev.key !== 'Enter' && ev.key !== ' ') return; 3487 let btn = null; 3488 if (ev.composedPath) { 3489 const path = ev.composedPath(); 3490 for (let i = 0; i < path.length; i++) { 3491 const n = path[i]; 3492 if (n && n.classList && n.classList.contains && n.classList.contains('widget-launcher-button')) { btn = n; break; } 3493 } 3494 } 3495 if (!btn && ev.target && ev.target.closest) { 3496 btn = ev.target.closest('.widget-launcher-button'); 3497 } 3498 if (btn) { 3499 installButtonListener(btn); 3500 // Delay mÃnimo para dar tiempo al widget a actualizar la clase (Chromium) 3501 setTimeout(syncChatState, 200); 3502 } 3503 } catch(_e){} 3504 }, true); 3505 // Bridge postMessage events from the widget iframe 3506 window.addEventListener('message', function(ev){ 3507 const d = ev.data; 3508 try { 3509 if (!d) return; 3510 if (typeof d === 'object') { 3511 const type = d.type || d.event || d.name || d.px_chat_event; 3512 if (!type) return; 3513 const payload = d.payload || d.meta || d; 3514 switch (String(type)) { 3515 case 'chat_opened': 3516 case 'widget_opened': 3517 case 'open': 3518 return handleChatOpen(payload); 3519 case 'message_sent': 3520 case 'chat_message': 3521 case 'message': 3522 case 'message_received': 3523 case 'new_message': 3524 // Extraer message_id del payload 3525 const messageId = payload && (payload.message_id || payload.messageId || payload.id || payload.chat_id); 3526 const messageType = payload && (payload.type || payload.message_type || 'text'); 3527 const from = payload && (payload.from || payload.user_id || payload.userId); 3528 if (messageId) { 3529 detectChatMessage(messageId, messageType, from); 3530 } else { 3531 return handleChatMessage(payload); 3532 } 3533 return; 3534 case 'chat_closed': 3535 case 'widget_closed': 3536 case 'close': 3537 return handleChatClose(payload); 3538 } 3539 // También buscar en propiedades del objeto directamente 3540 if (d.message_id || d.messageId || d.id) { 3541 const messageId = d.message_id || d.messageId || d.id; 3542 const messageType = d.type || d.message_type || 'text'; 3543 const from = d.from || d.user_id || d.userId; 3544 detectChatMessage(messageId, messageType, from); 3545 } 3546 } 3547 if (typeof d === 'string' && d.indexOf('px-chat:') === 0) { 3548 const parts = d.split(':'); 3549 const type = parts[1]; 3550 let payload = null; 3551 if (parts[2]) { try { payload = JSON.parse(parts.slice(2).join(':')); } catch(_e){} } 3552 if (type === 'open') return handleChatOpen(payload); 3553 if (type === 'message') { 3554 if (payload && (payload.message_id || payload.messageId || payload.id)) { 3555 detectChatMessage(payload.message_id || payload.messageId || payload.id, payload.type || 'text', payload.from); 3556 } else { 3557 return handleChatMessage(payload); 3558 } 3559 return; 3560 } 3561 if (type === 'close') return handleChatClose(payload); 3562 } 3563 } catch(_e){} 3564 }); 3565 // Message detection: interceptar mensajes del widget 3566 let lastMessageIds = new Set(); // Track mensajes ya procesados para evitar duplicados 3567 let lastMessageTime = 0; 3568 const MESSAGE_THROTTLE_MS = 500; // Máximo 1 mensaje cada 500ms 3569 3570 function detectChatMessage(messageId, messageType, from) { 3571 if (!messageId) { 3572 console.debug('[pxChat] detectChatMessage: no messageId provided'); 3573 return; 3574 } 3575 3576 // Evitar duplicados usando messageId (verificar primero) 3577 if (messageId && lastMessageIds.has(messageId)) { 3578 console.debug('[pxChat] detectChatMessage: message already processed', messageId); 3579 return; 3580 } 3581 3582 // Throttling: evitar procesar mensajes muy rápidos 3583 const now = Date.now(); 3584 const timeSinceLast = now - lastMessageTime; 3585 if (timeSinceLast < MESSAGE_THROTTLE_MS) { 3586 console.debug('[pxChat] detectChatMessage: throttled', timeSinceLast, 'ms since last'); 3587 return; 3588 } 3589 3590 // Permitir trackear mensajes incluso si el chat no está visualmente abierto 3591 // Los mensajes pueden llegar cuando el chat está minimizado pero activo 3592 // En Chromium, el chat puede estar activo sin que chatIsOpen sea true 3593 const hasChatSession = getChatSessionId() !== null; 3594 const shouldProcess = hasChatSession || chatIsOpen; 3595 3596 if (!shouldProcess) { 3597 // Si no hay sesión y el chat no está abierto, intentar una vez más después de un delay 3598 console.debug('[pxChat] detectChatMessage: no session/open, will retry', { hasChatSession, chatIsOpen }); 3599 setTimeout(function() { 3600 const retryHasSession = getChatSessionId() !== null; 3601 const retryIsOpen = chatIsOpen; 3602 if ((retryHasSession || retryIsOpen) && messageId && !lastMessageIds.has(messageId)) { 3603 console.debug('[pxChat] detectChatMessage: retry after delay', { retryHasSession, retryIsOpen }); 3604 detectChatMessage(messageId, messageType, from); 3605 } else { 3606 console.debug('[pxChat] detectChatMessage: retry failed - conditions not met'); 3607 } 3608 }, 1500); // Aumentado a 1.5s para dar más tiempo en Chromium 3609 return; 3610 } 3611
3612 // Limpiar Set si tiene más de 100 mensajes (evitar memory leak) 3613 if (lastMessageIds.size > 100) { 3614 lastMessageIds.clear(); 3615 } 3616 3617 if (messageId) { 3618 lastMessageIds.add(messageId); 3619 } 3620 lastMessageTime = now; 3621 3622 // Disparar evento 3623 try { 3624 const payload = { 3625 chat_id: messageId, 3626 message_type: messageType || 'text', 3627 from: from 3628 }; 3629 console.debug('[pxChat] detectChatMessage: processing message', payload); 3630 handleChatMessage(payload); 3631 } catch(e) { 3632 console.error('[pxChat] â Error processing message:', e); 3633 } 3634 } 3635 3636 // Listener para eventos personalizados que el widget podrÃa disparar 3637 document.addEventListener('px-chat-message', function(ev) { 3638 try { 3639 const data = ev.detail || ev.data || {}; 3640 detectChatMessage(data.message_id || data.messageId || data.id, data.type || data.message_type, data.from); 3641 } catch(_e){} 3642 }); 3643 3644 // Interceptar logs de console.log para detectar mensajes de Pusher 3645 // El widget loguea mensajes con formato especÃfico que podemos interceptar 3646 try { 3647 const originalConsoleLog = console.log; 3648 let isProcessing = false; // Flag para evitar recursión 3649 let processedMessageIds = new Set(); // Track mensajes ya procesados por el interceptor 3650 3651 console.log = function(...args) { 3652 // Llamar al console.log original primero (siempre) 3653 originalConsoleLog.apply(console, args); 3654 3655 // Evitar procesamiento recursivo 3656 if (isProcessing) return; 3657 3658 try { 3659 isProcessing = true; 3660 3661 let detectedMessageId = null; 3662 let detectedMessageType = 'text'; 3663 let detectedFrom = null; 3664 3665 // Buscar message_id en los argumentos directamente (mejorado para Chromium) 3666 for (let i = 0; i < args.length; i++) { 3667 const arg = args[i]; 3668 if (typeof arg === 'object' && arg !== null && !Array.isArray(arg)) { 3669 // Buscar directamente en el objeto 3670 if (arg.message_id || arg.messageId || arg.id) { 3671 detectedMessageId = arg.message_id || arg.messageId || arg.id; 3672 detectedMessageType = arg.type || arg.message_type || 'text'; 3673 detectedFrom = arg.from || arg.user_id || arg.userId || null; 3674 break; 3675 } 3676 // Buscar en rawData (estructura anidada común en Pusher) 3677 if (arg.rawData && typeof arg.rawData === 'object') { 3678 if (arg.rawData.message_id || arg.rawData.messageId || arg.rawData.id) { 3679 detectedMessageId = arg.rawData.message_id || arg.rawData.messageId || arg.rawData.id; 3680 detectedMessageType = arg.rawData.type || arg.rawData.message_type || 'text'; 3681 detectedFrom = arg.rawData.from || arg.rawData.user_id || arg.rawData.userId || null; 3682 break; 3683 } 3684 } 3685 // Buscar en propiedades anidadas (para Chromium que puede tener estructura diferente) 3686 try { 3687 for (let key in arg) { 3688 if (arg.hasOwnProperty(key) && typeof arg[key] === 'object' && arg[key] !== null) { 3689 if (arg[key].message_id || arg[key].messageId || arg[key].id) { 3690 detectedMessageId = arg[key].message_id || arg[key].messageId || arg[key].id; 3691 detectedMessageType = arg[key].type || arg[key].message_type || 'text'; 3692 detectedFrom = arg[key].from || arg[key].user_id || arg[key].userId || null; 3693 break; 3694 } 3695 } 3696 } 3697 if (detectedMessageId) break; 3698 } catch(_e) {} 3699 } 3700 } 3701 3702 // Si no encontramos en objetos, buscar en strings (solo si contiene patrones de Pusher) 3703 if (!detectedMessageId) { 3704 const logStr = args.map(arg => { 3705 if (typeof arg === 'string') return arg; 3706 if (typeof arg === 'object' && arg !== null) { 3707 try { return JSON.stringify(arg); } catch(_e) { return String(arg); } 3708 } 3709 return String(arg); 3710 }).join(' '); 3711 3712 // Solo procesar si contiene patrones especÃficos de Pusher 3713 if ((logStr.indexOf('[Pusher] ð¨ MENSAJE RECIBIDO') !== -1 || 3714 logStr.indexOf('[Pusher] ð¨ Mensaje recibido') !== -1 || 3715 logStr.indexOf('[Pusher] Body completo:') !== -1 || 3716 logStr.indexOf('[Pusher]') !== -1) && 3717 (logStr.indexOf('message_id') !== -1 || logStr.indexOf('messageId') !== -1)) { 3718 3719 // Intentar extraer message_id del string (múltiples formatos) 3720 try { 3721 // Formato: "message_id": "xxx" 3722 let messageIdMatch = logStr.match(/["']message_id["']\s*:\s
3722*["']([^"']+)["']/); 3723 if (!messageIdMatch) { 3724 // Formato: messageId: "xxx" 3725 messageIdMatch = logStr.match(/["']messageId["']\s*:\s*["']([^"']+)["']/); 3726 } 3727 if (!messageIdMatch) { 3728 // Formato: message_id: xxx (sin comillas) 3729 messageIdMatch = logStr.match(/message_id["']?\s*:\s*["']?([a-zA-Z0-9\-_]+)/); 3730 } 3731 if (messageIdMatch && messageIdMatch[1]) { 3732 detectedMessageId = messageIdMatch[1]; 3733 console.debug('[pxChat] Detected message_id from log string:', detectedMessageId); 3734 } 3735 } catch(_e) {} 3736 } 3737 } 3738 3739 // Procesar solo si encontramos un messageId y no lo hemos procesado antes 3740 if (detectedMessageId && !processedMessageIds.has(detectedMessageId)) { 3741 processedMessageIds.add(detectedMessageId); 3742 // Limpiar Set si tiene más de 50 mensajes 3743 if (processedMessageIds.size > 50) { 3744 processedMessageIds.clear(); 3745 } 3746 3747 console.debug('[pxChat] Detected message from Pusher log:', { 3748 messageId: detectedMessageId, 3749 messageType: detectedMessageType, 3750 from: detectedFrom 3751 }); 3752 3753 setTimeout(function() { 3754 detectChatMessage(detectedMessageId, detectedMessageType, detectedFrom); 3755 }, 300); // Aumentado a 300ms para dar tiempo a que se establezca la sesión 3756 } 3757 } catch(e) { 3758 // Silenciar errores en el interceptor para no romper otros logs 3759 console.debug('[pxChat] Error in console.log interceptor:', e); 3760 } finally { 3761 isProcessing = false; 3762 } 3763 }; 3764 3765 originalConsoleLog('[pxChat] Console.log interceptor installed for Pusher message detection'); 3766 } catch(e) { 3767 console.error('[pxChat] Error setting up console.log interceptor:', e); 3768 } 3769 3770 // MutationObserver para detectar mensajes en el DOM del chat (dentro del iframe) 3771 // Nota: Esto solo funciona si tenemos acceso al contenido del iframe (mismo origen) 3772 try { 3773 const chatMessageObserver = new MutationObserver(function(mutations) { 3774 if (!chatIsOpen) return; 3775 3776 for (let i = 0; i < mutations.length; i++) { 3777 const mutation = mutations[i]; 3778 if (mutation.addedNodes && mutation.addedNodes.length > 0) { 3779 for (let j = 0; j < mutation.addedNodes.length; j++) { 3780 const node = mutation.addedNodes[j]; 3781 if (node.nodeType === 1) { // Element node 3782 // Buscar elementos que parezcan mensajes de chat 3783 // Selectores comunes: .message, .chat-message, [data-message-id], etc. 3784 const messageEl = node.classList && ( 3785 node.classList.contains('message') || 3786 node.classList.contains('chat-message') || 3787 node.classList.contains('msg') || 3788 node.getAttribute('data-message-id') 3789 ) ? node : (node.querySelector && ( 3790 node.querySelector('.message') || 3791 node.querySelector('.chat-message') || 3792 node.querySelector('[data-message-id]') 3793 )); 3794 3795 if (messageEl) { 3796 const messageId = messageEl.getAttribute('data-message-id') || 3797 messageEl.getAttribute('data-id') || 3798 messageEl.id || 3799 null; 3800 const messageType = messageEl.getAttribute('data-type') || 3801 messageEl.getAttribute('data-message-type') || 3802 'text'; 3803 const from = messageEl.getAttribute('data-from') || null; 3804 3805 if (messageId) { 3806 detectChatMessage(messageId, messageType, from); 3807 } 3808 } 3809 } 3810 } 3811 } 3812 } 3813 }); 3814 3815 // Observar el iframe del chat cuando esté disponible 3816 function observeChatIframe() { 3817 const ifr = findChatIframe(); 3818 if (ifr && ifr.contentDocument && ifr.contentDocument.body) { 3819 try { 3820 chatMessageObserver.observe(ifr.contentDocument.body, { 3821 childList: true, 3822 subtree: true 3823 }); 3824 console.debug('[pxChat] Message observer installed on chat iframe'); 3825 } catch(e) { 3826 // Cross-origin: no podemos acceder al contenido del iframe 3827 console.debug('[pxChat] Cannot observe iframe content (cross-origin)'); 3828 } 3829 } 3830 } 3831 3832 // Intentar observar cuando el iframe se carga 3833 const ifr = findChatIframe(); 3834 if (ifr) { 3835 if (ifr.contentDocument && ifr.contentDocument.body) { 3836 observeChatIframe(); 3837 } else { 3838 ifr.addEventListener('load', observeChatIframe); 3839 } 3840 } 3841 3842 // También intentar cuando se detecta un nuevo iframe
3843 const iframeObserver = new MutationObserver(function() { 3844 const ifr = findChatIframe(); 3845 if (ifr && !ifr.__px_chat_message_observed) { 3846 ifr.__px_chat_message_observed = true; 3847 if (ifr.contentDocument && ifr.contentDocument.body) { 3848 observeChatIframe(); 3849 } else { 3850 ifr.addEventListener('load', observeChatIframe); 3851 } 3852 } 3853 }); 3854 iframeObserver.observe(document.body, { childList: true, subtree: true }); 3855 } catch(e) { 3856 console.error('[pxChat] Error setting up message observer:', e); 3857 } 3858 3859 // Expose helpers for manual triggers if needed 3860 window.pxChatOpen = function(meta){ handleChatOpen(meta || {}); }; 3861 window.pxChatMessage = function(meta){ handleChatMessage(meta || {}); }; 3862 window.pxChatClose = function(meta){ handleChatClose(meta || {}); }; 3863 // Expose message detection function for external use 3864 window.pxChatDetectMessage = function(messageId, messageType, from) { 3865 detectChatMessage(messageId, messageType, from); 3866 }; 3867 // Robust DOM observer: button presence and iframe lifecycle 3868 const observedIframes = new Set(); 3869 function isChatIframe(node){ 3870 if (!node || node.nodeName !== 'IFRAME') return false; 3871 try { 3872 const src = node.getAttribute('src') || ''; 3873 return /pxsol|chat|widget/i.test(src); 3874 } catch(_e){ return false; } 3875 } 3876 function installButtonListener(btn){ 3877 if (!btn || btn.__px_chat_hooked) return; 3878 btn.__px_chat_hooked = true; 3879 try { 3880 btn.__px_open_state = !!(btn.classList && btn.classList.contains('widget-launcher-button--open')); 3881 // NO sincronizar estado inicial aquà - solo observar cambios futuros 3882 let buttonChangeTimer = null; 3883 const ob = new MutationObserver(function(muts){ 3884 for (let i = 0; i < muts.length; i++){ 3885 const m = muts[i]; 3886 if (m.type === 'attributes' && m.attributeName === 'class') { 3887 const isOpen = btn.classList.contains('widget-launcher-button--open'); 3888 // Solo disparar si realmente cambió el estado 3889 if (isOpen !== btn.__px_open_state) { 3890 const now = Date.now(); 3891 if (now - lastStateChangeMs < 300) { 3892 console.debug('[pxChat] MutationObserver: change throttled'); 3893 return; // Ignorar cambios muy rápidos 3894 } 3895 3896 // Procesar inmediatamente (sin debounce para Chromium) 3897 if (buttonChangeTimer) { 3898 clearTimeout(buttonChangeTimer); 3899 } 3900 // Delay mÃnimo solo para asegurar que el DOM se actualizó 3901 buttonChangeTimer = setTimeout(function() { 3902 processButtonStateChange(btn, now); 3903 buttonChangeTimer = null; 3904 }, 50); // Delay mÃnimo de 50ms para Chromium 3905 } 3906 } 3907 } 3908 }); 3909 ob.observe(btn, { attributes: true, attributeFilter: ['class'] }); 3910 btn.__px_chat_observer = ob; 3911 } catch(_e){} 3912 } 3913 3914 function processButtonStateChange(btn, timestamp) { 3915 // Verificar que el estado realmente cambió 3916 const currentIsOpen = btn.classList.contains('widget-launcher-button--open') || 3917 btn.classList.toString().indexOf('--open') !== -1; 3918 console.log('[pxChat] processButtonStateChange:', { 3919 currentIsOpen: currentIsOpen, 3920 previousState: btn.__px_open_state, 3921 chatIsOpen: chatIsOpen 3922 }); 3923 3924 if (currentIsOpen === btn.__px_open_state) { 3925 console.log('[pxChat] MutationObserver: state unchanged, skipping'); 3926 return; 3927 } 3928 3929 btn.__px_open_state = currentIsOpen; 3930 lastStateChangeMs = timestamp || Date.now(); 3931 3932 if (currentIsOpen) { 3933 console.log('[pxChat] MutationObserver: button opened, calling handleChatOpen'); 3934 handleChatOpen(); 3935 } else { 3936 // Verificar que chatIsOpen es true antes de intentar cerrar 3937 if (!chatIsOpen) { 3938 console.log('[pxChat] MutationObserver: button closed but chatIsOpen is already false, skipping'); 3939 return; 3940 } 3941 // Guard mÃnimo: no cerrar si el chat se abrió hace menos de 3 segundos 3942 const finalNow = Date.now(); 3943 const startMs = chatOpenTsMs || lastOpenPushMs || null; 3944 if (startMs && (finalNow - startMs < 3000)) { 3945 console.log('[pxChat] MutationObserver: chat_close blocked - chat opened too recently (' + Math.round((finalNow - startMs) / 1000) + 's ago, minimum 3s)'); 3946 return; 3947 } 3948 3949 // CRÃTICO: Esperar y verificar nuevamente que el botón realmente perdió la clase --open 3950 // Esto previene disparos prematuros cuando el DOM aún se está actualizando 3951 setTimeout(function() { 3952 const btnCheck = document.querySelector('.widget-launcher-button') || 3953 document.querySelector('[class*="widget-launcher"]') || 3954 document.querySelector('[class*="launcher-button"]'); 3955 const isStillOpen = btnCheck && btnCheck.classList && ( 3956 btnCheck.classList.contains('widget-launcher-button--open') || 3957 btnCheck.classList.toString().indexOf('--open') !== -1 3958 ); 3959 3960 console.log('[pxChat] MutationObserver: delayed verification after button close:', { 3961 isStillOpen: isStillOpen, 3962 chatIsOpen: chatIsOpen, 3963 hasButton: !!btnCheck 3964 }); 3965 3966 // Solo proceder si el botón realmente está cerrado 3967 // Si hay botón, confiar solo en el botón (no en el iframe) 3968 if (isStillOpen) { 3969 console.log('[pxChat] MutationObserver: chat_close CANCELLED - button still shows as open after delay'); 3970 return; 3971 } 3972 // Solo verificar iframe si NO hay botón 3973 if (!btnCheck) { 3974 const ifr = findChatIframe(); 3975 const isIframeVisible = isElementVisible(ifr); 3976 if (isIframeVisible) { 3977 console.log('[pxChat] MutationObserver: chat_close CANCELLED - iframe still visible after delay (no button found)'); 3978 return; 3979 } 3980 } 3981 if (!chatIsOpen) { 3982 console.log('[pxChat] MutationObserver: chat_close CANCELLED - chatIsOpen is already false'); 3983 return; 3984 } 3985 3986 console.log('[pxChat] MutationObserver: button confirmed closed after delay, calling handleChatClose'); 3987 handleChatClose(); 3988 }, 600); // Esperar 600ms para confirmar que el DOM se actualizó completamente 3989 } 3990 } 3991
3992 function scanExisting(){ 3993 try { 3994 const btn = document.querySelector('.widget-launcher-button'); 3995 if (btn) installButtonListener(btn); 3996 document.querySelectorAll('iframe').forEach(function(ifr){ 3997 if (isChatIframe(ifr) && !observedIframes.has(ifr)) { 3998 observedIframes.add(ifr); 3999 // Delay más largo para evitar falsos positivos 4000 setTimeout(syncChatState, 800); 4001 } 4002 }); 4003 } catch(_e){} 4004 } 4005 const mo = new MutationObserver(function(mutations){ 4006 let anyRemoval = false; 4007 for (const m of mutations) { 4008 if (m.type === 'childList') { 4009 // Added nodes 4010 m.addedNodes && m.addedNodes.forEach(function(n){ 4011 try { 4012 if (!(n instanceof Element)) return; 4013 if (n.classList && n.classList.contains('widget-launcher-button')) { 4014 installButtonListener(n); 4015 } 4016 if (isChatIframe(n) && !observedIframes.has(n)) { observedIframes.add(n); setTimeout(syncChatState, 800); } 4017 // Deep scan 4018 n.querySelectorAll && n.querySelectorAll('.widget-launcher-button,iframe').forEach(function(x){ 4019 if (x.classList && x.classList.contains('widget-launcher-button')) installButtonListener(x); 4020 if (isChatIframe(x) && !observedIframes.has(x)) { observedIframes.add(x); setTimeout(syncChatState, 800); } 4021 }); 4022 } catch(_e){} 4023 }); 4024 // Removed nodes 4025 if (m.removedNodes && m.removedNodes.length) { 4026 anyRemoval = true; 4027 } 4028 } 4029 }
4030 if (anyRemoval) { 4031 // Check if any observed iframe is gone 4032 const still = new Set(); 4033 observedIframes.forEach(function(ifr){ 4034 if (document.body && document.body.contains(ifr)) { 4035 still.add(ifr); 4036 } else { 4037 // Delay más largo cuando se remueve el iframe (chat cerrado) 4038 setTimeout(syncChatState, 500); 4039 } 4040 }); 4041 observedIframes.clear(); 4042 still.forEach(function(x){ observedIframes.add(x); }); 4043 } 4044 }); 4045 // Poller de respaldo acotado: solo durante una ventana corta post-load 4046 // para evitar loops eternos si no aparece el botón. 4047 const CHAT_FALLBACK_POLL_INTERVAL_MS = 500; 4048 const CHAT_FALLBACK_POLL_MAX_MS_AFTER_LOAD = 2000; 4049 let chatFallbackPollIntervalId = null; 4050 let chatFallbackPollStartMs = null; 4051 4052 function stopChatFallbackPoller(reason) { 4053 if (!chatFallbackPollIntervalId) return; 4054 clearInterval(chatFallbackPollIntervalId); 4055 chatFallbackPollIntervalId = null; 4056 console.log('[pxChat] Polling stopped:', reason); 4057 } 4058 4059 function runChatFallbackPollTick() { 4060 const btn = document.querySelector('.widget-launcher-button') || 4061 document.querySelector('[class*="widget-launcher"]') || 4062 document.querySelector('[class*="launcher-button"]'); 4063 // Solo ejecutar syncChatState si NO hay botón (el botón tiene su propio observer) 4064 if (!btn) { 4065 console.log('[pxChat] Polling syncChatState (no button found)'); 4066 syncChatState(); 4067 return; 4068 } 4069 4070 console.debug('[pxChat] Polling skipped - button found, using MutationObserver instead'); 4071 stopChatFallbackPoller('button found'); 4072 } 4073 4074 function startChatFallbackPoller() { 4075 if (chatFallbackPollIntervalId) return; 4076 chatFallbackPollStartMs = Date.now(); 4077 runChatFallbackPollTick(); // Tick inicial inmediato 4078 chatFallbackPollIntervalId = setInterval(function() { 4079 if (Date.now() - chatFallbackPollStartMs > CHAT_FALLBACK_POLL_MAX_MS_AFTER_LOAD) { 4080 stopChatFallbackPoller('load + ' + CHAT_FALLBACK_POLL_MAX_MS_AFTER_LOAD + 'ms timeout'); 4081 return; 4082 } 4083 runChatFallbackPollTick(); 4084 }, CHAT_FALLBACK_POLL_INTERVAL_MS); 4085 } 4086 4087 // Ejecutar solo post-load (o inmediatamente si la página ya terminó de cargar) 4088 if (document.readyState === 'complete') { 4089 startChatFallbackPoller(); 4090 } else { 4091 window.addEventListener('load', function onLoadStartChatFallbackPoller() { 4092 window.removeEventListener('load', onLoadStartChatFallbackPoller); 4093 startChatFallbackPoller(); 4094 }); 4095 } 4096 try { 4097 mo.observe(document.documentElement || document, { childList: true, subtree: true }); 4098 // Initial scan after load tick 4099 if (document.readyState === 'loading') { 4100 document.addEventListener('DOMContentLoaded', scanExisting); 4101 } else { 4102 setTimeout(scanExisting, 0); 4103 } 4104 } catch(_e){} 4105 })(); 4106</script>
vendor: 1 bytes, line 4106
4106
4107<script> 4108 // Override para evitar espejo de chat_* al dataLayer y evitar doble track 4109 (function(){ 4110 if (typeof window.gtag === 'function') { 4111 window.gtag = function(){ 4112 try { window.dataLayer.push(arguments); } catch(_e){} 4113 }; 4114 } 4115 // Redefinir pxTrack: usa gtag si existe; sino, dataLayer 4116 window.pxTrack = function(eventName, params){ 4117 params = params || {}; 4118 if (typeof window.gtag === 'function') { 4119 try { window.gtag('event', eventName, params); } catch(_e){} 4120 } else { 4121 try { window.dataLayer.push(Object.assign({ event: eventName }, params)); } catch(_e){} 4122 } 4123 }; 4124 })(); 4125</script>
4125 4126</script>
4126 </div> 4127 <div id="CortinaCargando" align="center" class="Loading"> 4128 <div class="linear-activity"> 4129 <div class="indeterminate"></div> 4130 </div> 4131 </div> 4132 4133 <div id="diccionario"> 4134 </div>
4135<script> 4136 window.currentContent = "list"; 4137 window.currentDevice = "desktop"; 4138 console.log('CurrentState', window.currentContent, window.currentDevice); 4139</script>
4139 4140 4141
4141<script src="/neo_modules/neo_popup/js.js?v=121790891789"></script>
4141 4142<!-- End Popup code --> 4143<div class="EndError left full" style="max-width: 100vw; overflow: auto;"> 4144 </div> 4145 4146
4147<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"1a7d7c25cb794fbbb525883f1f41f61e","r":1,"spa":2}' crossorigin="anonymous"></script>
4147 4148</body> 4149<div id="dialogBox" title="..." class="nodisplay"> 4150 <p><span class="ui-icon ui-icon-alert alerticon"></span>...</p> 4151</div> 4152 4153 4154 4155 4156</html> 4157 4158
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.