1(function ($) { 2 'use strict'; 3 4 // ======================================== 5 // FUNCIÃN DE ESCAPE PARA SELECTORES 6 // ======================================== 7 8 /** 9 * Escapa caracteres especiales para usar en selectores jQuery 10 * @param {string} str - String a escapar 11 * @returns {string} - String escapado 12 */ 13 function escapeSelector(str) { 14 if (!str) return ''; 15 // Escapa: !"#$%&'()*+,./:;<=>?@[\]^`{|}~ 16 return String(str).replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, '\\$1'); 17 } 18 19 /** 20 * Normaliza un slug removiendo caracteres problemáticos para IDs 21 * @param {string} str - String a normalizar 22 * @returns {string} - String normalizado 23 */ 24 function normalizarSlugParaID(str) { 25 if (!str) return ''; 26 return String(str) 27 .toLowerCase() 28 .replace(/\s+/g, '-') // Espacios â guiones 29 .replace(/\//g, '-') // Slashes â guiones 30 .replace(/[^\w\-]+/g, '') // Remover caracteres especiales 31 .replace(/\-\-+/g, '-') // Múltiples guiones â uno solo 32 .replace(/^-+/, '') // Remover guiones al inicio 33 .replace(/-+$/, ''); // Remover guiones al final 34 } 35 36 let currentPage = 1; 37 let currentCaudal = 0; 38 let currentPresion = 0; 39 40 // â Variables para almacenar parámetros de búsqueda original 41 let busquedaOriginal = { 42 caudal: 0, 43 presion: 0, 44 toleranciaCaudal: 40, 45 toleranciaPresion: 40, 46 activa: false 47 }; 48 49 // â Variables para almacenar rangos originales (de la búsqueda base) 50 let rangosOriginales = { 51 precioMin: null, 52 precioMax: null, 53 potenciaMin: null, 54 potenciaMax: null, 55 guardados: false 56 }; 57 58 let RANGO_PRECIO = {min: 0, max: 0, step: 1000}; 59 let RANGO_POTENCIA = {min: 0, max: 0, step: 0.1}; 60 61 const MATERIALS_WITH_TABLE = ['PVC', 'HDPE', 'PEAD', 'PP-R', 'PPR', 'PP R']; 62 const OTRO_VALUE = 'Otro'; 63 64 const INTERNAL_DIAM_TABLE = { 65 'PVC': { 66 'DN20': {'PN10': 16.6, 'PN16': 16.6}, 67 'DN25': {'PN10': 21.6, 'PN16': 20.8}, 68 'DN32': {'PN10': 28.4, 'PN16': 26.6}, 69 'DN40': {'PN10': 35.8, 'PN16': 33.4}, 70 'DN50': {'PN10': 44.6, 'PN16': 42.0}, 71 'DN63': {'PN10': 56.4, 'PN16': 52.8}, 72 'DN75': {'PN10': 67.2, 'PN16': 63.0}, 73 'DN90': {'PN10': 80.6, 'PN16': 78.0}, 74 'DN110': {'PN10': 100.8, 'PN16': 95.8}, 75 'DN125': {'PN10': 114.6, 'PN16': 109.2}, 76 'DN140': {'PN10': 128.4, 'PN16': 122.2}, 77 'DN160': {'PN10': 146.6, 'PN16': 139.8}, 78 'DN180': {'PN10': 165.2, 'PN16': 157.2}, 79 'DN200': {'PN10': 183.6, 'PN16': 174.8}, 80 'DN250': {'PN10': 229.6, 'PN16': 218.6} 81 }, 82 'HDPE': { 83 'DN20': {'PN10': 16.0, 'PN16': 15.4}, 84 'DN25': {'PN10': 21.0, 'PN16': 19.0}, 85 'DN32': {'PN10': 27.2, 'PN16': 24.8}, 86 'DN40': {'PN10': 34.0, 'PN16': 31.0}, 87 'DN50': {'PN10': 42.6, 'PN16': 38.8}, 88 'DN63': {'PN10': 53.6, 'PN16': 48.8}, 89 'DN75': {'PN10': 63.8, 'PN16': 58.2}, 90 'DN90': {'PN10': 76.6, 'PN16': 69.8}, 91 'DN110': {'PN10': 93.8, 'PN16': 85.4}, 92 'DN125': {'PN10': 106.6, 'PN16': 97.0}, 93 'DN140': {'PN10': 119.4, 'PN16': 122.2}, 94 'DN160': {'PN10': 136.4, 'PN16': 124.2}, 95 'DN180': {'PN10': 153.4, 'PN16': 139.8}, 96 'DN200': {'PN10': 170.6, 'PN16': 155.2}, 97 'DN250': {'PN10': 213.2, 'PN16': 194.2} 98 }, 99 'PP-R': { 100 'DN20': {'PN10': 16.2, 'PN16': 14.4}, 101 'DN25': {'PN10': 20.4, 'PN16': 18.0}, 102 'DN32': {'PN10': 26.2, 'PN16': 23.2}, 103 'DN40': {'PN10': 32.6, 'PN16': 29.0}, 104 'DN50': {'PN10': 40.8, 'PN16': 36.2}, 105 'DN63': {'PN10': 51.4, 'PN16': 45.8}, 106 'DN75': {'PN10': 61.4, 'PN16': 54.4}, 107 'DN90': {'PN10': 73.6, 'PN16': 65.4}, 108 'DN110': {'PN10': 90.0, 'PN16': 79.8} 109 } 110 }; 111 112 // ======================================== 113 // VARIABLE GLOBAL PARA TIPOS ORIGINALES 114 // ======================================== 115 window.__TIPOS_ORIGINALES__ = []; 116 117 function normMaterial(val) { 118 if (!val) return ''; 119 const v = String(val).trim().toUpperCase(); 120 if (['PEAD', 'HDPE'].includes(v)) return 'HDPE'; 121 if (['PPR', 'PP R', 'PP-R', 'PPâR'].includes(v)) return 'PP-R'; 122 return v; 123 } 124 125 function isMaterialWithTable(material) { 126 const m = normMaterial(material); 127 return MATERIALS_WITH_TABLE.map(normMaterial).includes(m); 128 } 129 130 function normDnKey(raw) { 131 if (!raw) return ''; 132 const s = String(raw).toUpperCase().replace(/\s+/g, ''); 133 if (/^DN\d+/.test(s)) return s.replace(/[^DN0-9]/g, ''); 134 const m = s.match(/(\d{1,3})/); 135 return m ? ('DN' + m[1]) : ''; 136 } 137 138 function normPnKey(raw) { 139 if (!raw) return ''; 140 const s = String(raw).toUpperCase().replace(/\s+/g, ''); 141 if (/^PN\d+/.test(s)) return s.replace(/[^PN0-9]/g, ''); 142 const m = s.match(/(\d{1,2})/); 143 return m ? ('PN' + m[1]) : ''; 144 } 145 146 function getDnEl() { 147 let $el = $('#diametro_nominal_tuberia'); 148 if (!$el.length) $el = $('#dn'); 149 if (!$el.length) $el = $('[name="diametro_nominal_tuberia"]'); 150 if (!$el.length) $el = $('[name="dn"]'); 151 return $el; 152 } 153 154 function getPnEl() { 155 let $el = $('#presion_nominal_tuberia'); 156 if (!$el.length) $el = $('#presion_nominal'); 157 if (!$el.length) $el = $('[name="presion_nominal_tuberia"]'); 158 if (!$el.length) $el = $('[name="presion_nominal"]'); 159 return $el; 160 } 161 162 function setDiametroEditableAndRequired(editable, required) { 163 required = required || false; 164 const $inp = $('#diametro_interior'); 165 if (!$inp.length) return $(); 166 167 $inp.prop('readonly', !editable) 168 .toggleClass('is-readonly', !editable) 169 .toggleClass('calc-input-readonly', !editable) 170 .toggleClass('calc-input', editable); 171 172 if (required && editable) { 173 $inp.attr('required', 'required'); 174 const $label = $('label[for="diametro_interior"]'); 175 if ($label.length && !$label.find('.required-mark').length) { 176 const labelText = $label.text().replace(' (auto)', '').replace('*', ''); 177 $label.html(labelText + ' <span class="required-mark" style="color: red;">*</span>'); 178 } 179 } else { 180 $inp.removeAttr('required'); 181 const $label = $('label[for="diametro_interior"]'); 182 if ($label.length) { 183 const labelText = $label.text().replace('*', ''); 184 } 185 } 186 return $inp; 187 } 188 189 function showDnPnBlock(visible) { 190 const $dn = getDnEl(); 191 const $pn = getPnEl(); 192 if (!$dn.length && !$pn.length) return; 193
194 function findWrapper($el) { 195 if (!$el || !$el.length) return $(); 196 let $w = $el.closest('.bloque-nominal-presion, .form-group, .calc-form-group, .form-row, .row, .col, .col-12, .col-md-6, .col-md-12'); 197 if ($w.length) return $w; 198 const $label = $el.prev('label'); 199 return $el.add($label); 200 } 201 202 const $dnWrap = findWrapper($dn); 203 const $pnWrap = findWrapper($pn); 204 if ($dnWrap.length) $dnWrap.toggle(visible).toggleClass('d-none', !visible); 205 if ($pnWrap.length) $pnWrap.toggle(visible).toggleClass('d-none', !visible); 206 } 207 208 function validatePresionForm() { 209 const $input = $('#diametro_interior'); 210 if (!$input.length) return true; 211 if ($input.attr('required') && !$input.val()) { 212 $input.addClass('is-invalid'); 213 if (!$input.next('.invalid-feedback').length) { 214 $input.after('<div class="invalid-feedback">El diámetro interior es requerido cuando se selecciona "Otro"</div>'); 215 } 216 $input.focus(); 217 return false; 218 } 219 $input.removeClass('is-invalid'); 220 $input.next('.invalid-feedback').remove(); 221 return true; 222 } 223 224 const Modal = (function () { 225 let scrollTop = 0; 226 let $active = null; 227 let lastFocused = null; 228 229 function lockScroll() { 230 scrollTop = window.pageYOffset || document.documentElement.scrollTop; 231 $('body').addClass('modal-open').css({position: 'fixed', top: -scrollTop + 'px', width: '100%'}); 232 } 233 234 function unlockScroll() { 235 $('body').removeClass('modal-open').attr('style', ''); 236 window.scrollTo(0, scrollTop || 0); 237 } 238 239 function getFocusable($root) { 240 return $root.find('a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])').filter(':visible'); 241 } 242 243 function onKeyDown(e) { 244 if (!$active) return; 245 if (e.key === 'Escape') { 246 e.preventDefault(); 247 hide($active.attr('id')); 248 return; 249 } 250 if (e.key === 'Tab') { 251 const $focus = getFocusable($active); 252 if (!$focus.length) return; 253 const first = $focus[0], last = $focus[$focus.length - 1]; 254 if (e.shiftKey && document.activeElement === first) { 255 e.preventDefault(); 256 last.focus(); 257 } else if (!e.shiftKey && document.activeElement === last) { 258 e.preventDefault(); 259 first.focus(); 260 } 261 } 262 } 263 264 function bindGlobal() { 265 $(document).on('keydown.modal', onKeyDown); 266 $(document).on('click.modal', '.calc-modal', function (e) { 267 if ($(e.target).hasClass('calc-modal')) hide(this.id); 268 }); 269 } 270 271 function unbindGlobal() { 272 $(document).off('keydown.modal'); 273 $(document).off('click.modal'); 274 } 275 276 function show(id) { 277 const $m = $('#' + id); 278 if (!$m.length) return; 279 if ($active && $active[0].id === id) return; 280 lastFocused = document.activeElement; 281 $active = $m; 282 lockScroll(); 283 bindGlobal(); 284 $m.addClass('active').fadeIn(120, function () { 285 const $content = $m.find('.calc-modal-content').first(); 286 const $auto = $content.find('[autofocus]').first(); 287 const $f = getFocusable($content); 288 ($auto.length ? $auto : $f.first()).trigger('focus'); 289 }); 290 $m.on('click.modalclose', '.calc-modal-close,[data-dismiss="modal"]', function (e) { 291 e.preventDefault(); 292 hide(id); 293 }); 294 } 295 296 function hide(id) { 297 const $m = id ? $('#' + id) : $active; 298 if (!$m || !$m.length) return; 299 $m.off('click.modalclose'); 300 $m.fadeOut(120, function () { 301 $m.removeClass('active'); 302 $active = null; 303 unbindGlobal(); 304 unlockScroll(); 305 if (lastFocused && typeof lastFocused.focus === 'function') lastFocused.focus(); 306 }); 307 } 308 309 return {show: show, hide: hide}; 310 })(); 311 312 // ======================================== 313 // GUARDAR TIPOS ORIGINALES (UNA SOLA VEZ) 314 // ======================================== 315 function guardarTiposOriginales() { 316 if (window.__TIPOS_ORIGINALES__.length === 0) { 317 const $container = $('#filtro-tipo-container'); 318 $container.find('.filtro-checkbox').not(':first').each(function () { 319 const checkbox = $(this).find('input[type="checkbox"]'); 320 const tipo = { 321 slug: checkbox.val(), 322 nombre: $(this).find('label').text().trim() 323 }; 324 window.__TIPOS_ORIGINALES__.push(tipo); 325 }); 326 console.log('ð¾ Tipos originales guardados:', window.__TIPOS_ORIGINALES__); 327 } 328 } 329 330 $(document).ready(function () { 331 initCalculadora(); 332 initModales(); 333 initFiltros(); 334 updateFlowRateUnit(0); 335 336 applyMaterialVisibility(); 337 tryAutofillDiametro(); 338 339 // Inicializa Select2 del select de marcas siempre que exista 340 initSelect2Marca(); 341 342 // Inicializar eventos de marca 343 bindMarcaCheckboxEvents(); 344 345 // â GUARDAR TIPOS ORIGINALES AL CARGAR 346 setTimeout(function () { 347 guardarTiposOriginales(); 348 console.log('â Tipos originales capturados al iniciar'); 349 }, 1000); 350 351 (function watchDnPnFields() { 352 let checks = 0; 353 const interval = setInterval(function () { 354 const $dn = getDnEl(); 355 const $pn = getPnEl(); 356 const need = ($dn.length || $pn.length); 357 if (need) { 358 applyMaterialVisibility(); 359 tryAutofillDiametro(); 360 clearInterval(interval); 361 } 362 if (++checks > 30) clearInterval(interval); 363 }, 400); 364 })(); 365 }); 366 367 $(document).on('click', '[data-unit]', function (e) { 368 e.preventDefault(); 369 updateFlowRateUnit($(this).data('unit')); 370 }); 371 372 function updateFlowRateUnit(unit) { 373 const idxToKey = ['lmin', 'ls', 'm3h'];
374 const key = (unit === 0 || unit === 1 || unit === 2) ? idxToKey[unit] : String(unit); 375 const labelMap = {lmin: '[l/min]', ls: '[l/s]', m3h: '[m³/h]'}; 376 const label = labelMap[key] || '[l/min]'; 377 $('#flowUnitLabel').text(label); 378 $('#flowRateUnit').val(key); 379 const isLmin = key === 'lmin'; 380 const placeholderMap = {lmin: 'Ej: 50', ls: 'Ej: 0.83', m3h: 'Ej: 3'}; 381 $('#input_caudal').attr('placeholder', isLmin ? placeholderMap[key] : 'Cambiar a l/min para calcular'); 382 const $calcBtns = $('#btn-modal-caudal'); 383 $calcBtns.prop('disabled', !isLmin).toggleClass('disabled', !isLmin) 384 .attr('title', isLmin ? '' : 'La calculadora de artefactos solo funciona con l/min. Cambie la unidad a l/min para usar esta función.'); 385 } 386 387 function initModales() { 388 $(document).on('click', '#btn-modal-caudal', function (e) { 389 e.preventDefault(); 390 Modal.show('modal-calcular-caudal'); 391 setTimeout(calcularCaudalArtefactosTiempoReal, 100); 392 }); 393 $(document).on('click', '#btn-modal-presion', function (e) { 394 e.preventDefault(); 395 const caudalInput = parseFloat($('#input_caudal').val()) || 0; 396 if (caudalInput <= 0) { 397 alert('â ï¸ CAUDAL REQUERIDO\n\nPor favor ingrese un valor de caudal antes de abrir la calculadora de presión.'); 398 $('#input_caudal').addClass('is-invalid'); 399 $('html, body').animate({scrollTop: $('#input_caudal').offset().top - 150}, 500, function () { 400 $('#input_caudal').focus(); 401 }); 402 return; 403 } 404 Modal.show('modal-calcular-presion'); 405 setTimeout(calcularPresionTiempoReal, 100); 406 }); 407 $(document).on('click', '.calc-modal-content', function (e) { 408 e.stopPropagation(); 409 }); 410 } 411 412 function initCalculadora() { 413 $(document).on('change', '#material', function () { 414 applyMaterialVisibility(); 415 tryAutofillDiametro(); 416 calcularPresionTiempoReal(); 417 }); 418 $(document).on('change', '#diametro_nominal_tuberia, #presion_nominal_tuberia, #dn, #presion_nominal', function () { 419 tryAutofillDiametro(); 420 calcularPresionTiempoReal(); 421 }); 422 $(document).on('input', '#diametro_interior, #input_caudal', function () { 423 if ($(this).val()) { 424 $(this).removeClass('is-invalid'); 425 $(this).next('.invalid-feedback').remove(); 426 } 427 }); 428 $(document).on('input change', '#ducha, #lavamanos, #wc, #lavaplatos, #lavadora, #lavavajillas, #llavejardin', function () { 429 calcularCaudalArtefactosTiempoReal(); 430 }); 431 $(document).on('input change', '#input_caudal, #material, #presion_nominal, #presion_nominal_tuberia, #largo_total, #altura_aspiracion, #altura_impulsion, #accesorios, #presion_util, #diametro_interior', function () { 432 calcularPresionTiempoReal(); 433 }); 434 $(document).on('submit', '#form-calcular-caudal', function (e) { 435 e.preventDefault(); 436 calcularArtefactos(); 437 }); 438 $(document).on('submit', '#form-calcular-presion', function (e) { 439 e.preventDefault(); 440 if (validatePresionForm()) { 441 calcularCaudal(); 442 } 443 }); 444 $(document).on('click', '#btn-buscar-bombas, #btn-buscar-bombas-main', function () { 445 currentPage = 1; 446 buscarBombas(true); 447 }); 448 $(document).on('click', '.pagination-btn', function () { 449 if (!$(this).hasClass('active') && !$(this).prop('disabled')) { 450 currentPage = parseInt($(this).data('page')); 451 buscarBombas(); 452 $('html, body').animate({scrollTop: $('#resultados-bombas').offset().top - 100}, 500); 453 } 454 }); 455 } 456 457 function applyMaterialVisibility() { 458 const $input = $('#diametro_interior'); 459 if (!$input.length) return; 460 const mat = $('#material').val(); 461 const usesTable = isMaterialWithTable(mat); 462 if (usesTable) { 463 showDnPnBlock(true); 464 tryAutofillDiametro(); 465 } else { 466 showDnPnBlock(false); 467 $input.val(''); 468 setDiametroEditableAndRequired(true, true).attr('placeholder', 'Ingrese diámetro interno (mm) *'); 469 } 470 } 471 472 function tryAutofillDiametro() { 473 const $input = $('#diametro_interior'); 474 if (!$input.length) return; 475 const mat = normMaterial($('#material').val()); 476 const $dn = getDnEl(); 477 const $pn = getPnEl(); 478 const dnRaw = $dn.val(); 479 const pnRaw = $pn.val(); 480 481 const usesTable = isMaterialWithTable(mat); 482 if (!usesTable) { 483 setDiametroEditableAndRequired(true, true).attr('placeholder', 'Ingrese diámetro interno (mm) *'); 484 return; 485 } 486 if (dnRaw === OTRO_VALUE || pnRaw === OTRO_VALUE) { 487 $input.val(''); 488 setDiametroEditableAndRequired(true, true).attr('placeholder', 'Ingrese diámetro interno (mm) *'); 489 return; 490 } 491 if (!dnRaw || !pnRaw) { 492 $input.val(''); 493 setDiametroEditableAndRequired(true, false).attr('placeholder', 'Seleccione DN y PN o ingrese manualmente'); 494 return; 495 } 496 const dnKey = normDnKey(dnRaw); 497 const pnKey = normPnKey(pnRaw); 498 const diamTabla = INTERNAL_DIAM_TABLE?.[mat]?.[dnKey]?.[pnKey]; 499 if (typeof diamTabla === 'number' && !Number.isNaN(diamTabla)) { 500 $input.val(String(diamTabla)); 501 setDiametroEditableAndRequired(false, false).attr('placeholder', ''); 502 return; 503 } 504 $input.val(''); 505 setDiametroEditableAndRequired(true, true).attr('placeholder', 'Ingrese diámetro interno (mm) *'); 506 } 507 508 function calcularCaudalArtefactosTiempoReal() { 509 const consumos = { 510 ducha: 12, 511 lavamanos: 8, 512 wc: 10, 513 lavaplatos: 12, 514 lavadora: 15, 515 lavavajillas: 12, 516 llavejardin: 9 517 }; 518 const cantidades = { 519 ducha: parseInt($('#ducha').val()) || 0, 520 lavamanos: parseInt($('#lavamanos').val()) || 0, 521 wc: parseInt($('#wc').val()) || 0, 522 lavaplatos: parseInt($('#lavaplatos').val()) || 0, 523 lavadora: parseInt($('#lavadora').val()) || 0, 524 lavavajillas: parseInt($('#lavavajillas').val()) || 0, 525 llavejardin: parseInt($('#llavejardin').val()) || 0 526 }; 527 528 let caudalTotal = 0; 529 for (let k in cantidades) caudalTotal += cantidades[k] * (consumos[k] || 0); 530 531 const caudalReal = 1.7391 * Math.pow(caudalTotal, 0.6891); 532 $('#resultado_caudal_valor').text(caudalReal.toFixed(1)); 533 return caudalReal; 534 } 535 536 function calcularPresionTiempoReal() { 537 const $input = $('#diametro_interior'); 538 if (!$input.length) return; 539 540 const mat = $('#material').val(); 541 const usesTable = isMaterialWithTable(mat); 542 const dnVal = String(getDnEl().val() || '').trim(); 543 const pnVal = String(getPnEl().val() || '').trim(); 544 const diametro = parseFloat($input.val()) || 0; 545 546 if (usesTable) { 547 if (dnVal === OTRO_VALUE || pnVal === OTRO_VALUE) { 548 if (!diametro) return; 549 } 550 } else { 551 if (!diametro) return; 552 } 553 554 const largo = parseFloat($('#largo_total').val()) || 0; 555 const aspiracion = parseFloat($('#altura_aspiracion').val()) || 0; 556 const impulsion = parseFloat($('#altura_impulsion').val()) || 0; 557 const presionUtil = parseFloat($('#presion_util').val()) || 0; 558 559 let caudalInput = parseFloat($('#input_caudal').val()) || 0; 560 if (caudalInput <= 0) { 561 $('#altura_estatica_valor').text('--'); 562 $('#perdida_friccion_valor').text('--'); 563 $('#velocidad_agua_valor').text('--'); 564 $('#presion_total_valor').text('--'); 565 return; 566 } 567 if (diametro <= 0 || largo <= 0) { 568 $('#altura_estatica_valor').text('--'); 569 $('#perdida_friccion_valor').text('--'); 570 $('#velocidad_agua_valor').text('--'); 571 $('#presion_total_valor').text('--'); 572 return; 573 } 574 575 const coefHW = { 576 'PVC': 150, 'HDPE': 150, 'PEAD': 150, 'PP-R': 150, 'PPR': 150, 577 'Acero Galvanizado': 120, 'Hierro Fundido': 100, 'Cemento': 110, 578 'Plástico Fibroreforzado (FRP)': 150 579 }; 580 const C = coefHW[mat] || coefHW[normMaterial(mat)] || 120; 581
582 const factorAccesorios = {'Baja': 1.1, 'Media': 1.25, 'Alta': 1.5}; 583 const K = 1.139; 584 585 const unit = $('#flowRateUnit').val() || 'lmin'; 586 let caudalLMin = caudalInput; 587 if (unit === 'ls') caudalLMin = caudalInput * 60; 588 if (unit === 'm3h') caudalLMin = caudalInput * 16.667; 589 590 const Q_m3s = caudalLMin / 60000; 591 const D_m = diametro / 1000; 592 593 const numerador = 10.67 * largo * Math.pow(Q_m3s, 1.852); 594 const denominador = Math.pow(C, 1.852) * Math.pow(D_m, 4.87); 595 const hf = numerador / denominador; 596 const hf_total = hf * K; 597 598 const area = Math.PI * Math.pow(D_m / 2, 2); 599 const velocidad = Q_m3s / area; 600 601 const h_estatica = aspiracion + impulsion; 602 const H_total = h_estatica + hf_total + presionUtil; 603 604 $('#altura_estatica_valor').text(h_estatica.toFixed(2)); 605 $('#perdida_friccion_valor').text(hf_total.toFixed(2)); 606 $('#velocidad_agua_valor').text(velocidad.toFixed(2)); 607 $('#presion_total_valor').text(H_total.toFixed(1)); 608 609 return H_total; 610 } 611 612 function calcularCaudal() { 613 const caudalInput = parseFloat($('#input_caudal').val()) || 0; 614 if (caudalInput <= 0) { 615 alert('â ï¸ CAUDAL REQUERIDO\n\nPor favor ingrese un valor de caudal antes de calcular la presión.'); 616 Modal.hide('modal-calcular-presion'); 617 $('#input_caudal').addClass('is-invalid'); 618 setTimeout(function () { 619 $('html, body').animate({scrollTop: $('#input_caudal').offset().top - 150}, 500, function () { 620 $('#input_caudal').focus(); 621 }); 622 }, 300); 623 return; 624 } 625 if (!validatePresionForm()) { 626 alert('Por favor complete el diámetro interior cuando DN o PN son "Otro"'); 627 return; 628 } 629 const presionCalculada = calcularPresionTiempoReal(); 630 if (presionCalculada && presionCalculada > 0) { 631 currentPresion = presionCalculada; 632 $('#input_presion').val(currentPresion.toFixed(1)); 633 Modal.hide('modal-calcular-presion'); 634 mostrarMensaje('â Presión aplicada: ' + currentPresion.toFixed(1) + ' mca'); 635 setTimeout(function () { 636 $('html, body').animate({scrollTop: $('.koslan-calculadora-hero').offset().top - 100}, 500); 637 }, 300); 638 } else { 639 alert('Por favor completa todos los campos requeridos'); 640 } 641 } 642 643 function calcularArtefactos() { 644 const caudalCalculado = calcularCaudalArtefactosTiempoReal(); 645 if (caudalCalculado && caudalCalculado > 0) { 646 currentCaudal = caudalCalculado; 647 $('#input_caudal').val(currentCaudal.toFixed(1)); 648 Modal.hide('modal-calcular-caudal'); 649 mostrarMensaje('â Caudal aplicado: ' + currentCaudal.toFixed(1) + ' L/min'); 650 setTimeout(function () { 651 $('html, body').animate({scrollTop: $('.koslan-calculadora-hero').offset().top - 100}, 500); 652 }, 300); 653 } else { 654 alert('Por favor ingresa al menos un artefacto'); 655 } 656 } 657 658 function fmtCLP(n) { 659 const v = Math.round(n || 0); 660 try { 661 return new Intl.NumberFormat('es-CL', { 662 style: 'currency', 663 currency: 'CLP', 664 maximumFractionDigits: 0 665 }).format(v); 666 } catch (e) { 667 return '$ ' + (v.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.')); 668 } 669 } 670 671 // ======================================== 672 // FUNCIONES CORRECTORAS PARA MARCAS 673 // ======================================== 674 675 /** 676 * â Actualiza marcas disponibles en mobile ocultando las no disponibles 677 */ 678 function actualizarMarcasMobileDisponibles(marcasDisponibles) { 679 console.log('ð·ï¸ Actualizando marcas mobile disponibles:', marcasDisponibles); 680 681 // Actualizar en desktop también (checkboxes mobile del HTML original) 682 const $containerDesktop = $('#filtro-marca-container'); 683 if ($containerDesktop.length) { 684 actualizarVisibilidadMarcas($containerDesktop, marcasDisponibles); 685 } 686 687 // Actualizar en mobile offcanvas si existe 688 const $containerMobile = $('#filtros-mobile #filtro-marca-container_mobile'); 689 if ($containerMobile.length) { 690 actualizarVisibilidadMarcas($containerMobile, marcasDisponibles); 691 } 692 } 693 694 /** 695 * â Actualiza visibilidad de checkboxes de marca según disponibilidad 696 */ 697 function actualizarVisibilidadMarcas($container, marcasDisponibles) { 698 if (!$container || !$container.length) return; 699 700 const disponiblesSet = new Set( 701 marcasDisponibles.map(m => { 702 if (typeof m === 'object' && m !== null) { 703 return m.slug || m.value || m.id || ''; 704 } 705 return String(m); 706 }) 707 ); 708 709 console.log('ð Marcas disponibles:', Array.from(disponiblesSet)); 710 711 // Actualizar visibilidad de cada checkbox 712 $container.find('.filtro-checkbox[data-marca-slug]').each(function () { 713 const marcaSlug = $(this).data('marca-slug'); 714 const estaDisponible = disponiblesSet.has(marcaSlug); 715 716 if (estaDisponible) { 717 $(this).removeClass('marca-no-disponible').show(); 718 $(this).find('input').prop('disabled', false); 719 } else { 720 $(this).addClass('marca-no-disponible').hide(); 721 $(this).find('input').prop('checked', false).prop('disabled', true); 722 } 723 }); 724 725 const visibles = $container.find('.filtro-checkbox[data-marca-slug]:visible').length; 726 console.log('ð Marcas visibles:', visibles); 727 } 728 729 /** 730 * Actualiza el select de marcas con los datos de la API (DESKTOP -
730Select2) 731 * â FIX: Ahora solo muestra las marcas disponibles según el tipo de bomba seleccionado 732 * y las selecciona automáticamente en lugar de mantener la selección anterior 733 */ 734 function actualizarSelectMarcasAPI(marcasArray, seleccionApi) { 735 const $select = $('#select-marca'); 736 if (!$select.length) return; 737 738 // Si no viene array válido, no tocar lo que está (CPT) 739 if (!Array.isArray(marcasArray) || !marcasArray.length) { 740 console.warn('â ï¸ No se recibieron marcas de la API'); 741 return; 742 } 743 744 console.log('ð·ï¸ Actualizando marcas con:', marcasArray.length, 'opciones disponibles'); 745 746 // Normaliza items 747 const apiItems = marcasArray 748 .map(m => { 749 const slug = (m && (m.slug || m.value || m.id || '')).toString().trim(); 750 const nombre = (m && (m.nombre || m.name || m.label || slug)).toString().trim(); 751 return slug ? {slug, nombre} : null; 752 }) 753 .filter(Boolean); 754 755 if (!apiItems.length) { 756 console.warn('â ï¸ No hay marcas válidas después de normalizar'); 757 return; 758 } 759 760 // Deshacer Select2 si estaba activo 761 const estabaSelect2 = $select.hasClass('select2-hidden-accessible'); 762 763 if (estabaSelect2) { 764 try { 765 $select.select2('destroy'); 766 } catch (e) { 767 console.error('Error al destruir Select2:', e); 768 } 769 } 770 771 // â FIX CRÃTICO: Limpiar TODAS las opciones excepto la opción "todas" 772 $select.find('option').not('[value="todos"]').remove(); 773 774 // Asegurar opción "todas" 775 const valorTodos = 'todos'; 776 if (!$select.find(`option[value="${valorTodos}"]`).length) { 777 $select.prepend(new Option('Todas las marcas', valorTodos, false, false)); 778 } 779 780 // Rehabilitar select 781 $select.prop('disabled', false); 782 783 // â FIX: Agregar SOLO las marcas disponibles según el tipo de bomba 784 apiItems.forEach(({slug, nombre}) => { 785 $select.append(new Option(nombre, slug, false, false)); 786 }); 787 788 // â SIEMPRE SELECCIONAR TODAS las marcas disponibles 789 // Ignoramos seleccionApi porque queremos que el usuario vea TODAS las opciones disponibles 790 // Si quiere filtrar por marca especÃfica, puede deseleccionar las que no quiere 791 const apiSlugs = apiItems.map(i => i.slug); 792 const targetSeleccion = apiSlugs.slice(); // Siempre todas 793 794 console.log('â Seleccionando TODAS las marcas disponibles:', targetSeleccion); 795 796 // Aplicar selección (asegurar multiple) 797 $select.prop('multiple', true); 798 $select.val(targetSeleccion); 799 800 // Re-inicializar Select2 801 if (typeof $.fn.select2 !== 'undefined') { 802 $select.select2({ 803 placeholder: 'Seleccionar marcas', 804 allowClear: true, 805 width: '100%', 806 language: { 807 noResults: function () { 808 return "No se encontraron marcas"; 809 }, 810 searching: function () { 811 return "Buscando..."; 812 } 813 } 814 }); 815 console.log('â Select2 reinicializado con TODAS las marcas seleccionadas'); 816 } 817 818 $select.trigger('change'); 819 820 // â Actualizar checkboxes mobile con visibilidad 821 actualizarMarcasMobileDisponibles(marcasArray); 822 823 // â Sincronizar: Como SIEMPRE seleccionamos TODAS, marcar "Todas" en mobile 824 $('#filtro-marca-container input[name="marca_checkbox[]"]').not('#marca_todas').prop('checked', false); 825 $('#marca_todas').prop('checked', true); 826 827 console.log('â Mobile sincronizado: "Todas" marcado'); 828 } 829 830 // â Exponer globalmente para que busqueda.php pueda llamarla 831 window.actualizarSelectMarcasAPI = actualizarSelectMarcasAPI; 832 833 /** 834 * Actualiza los checkboxes de tipo de bomba desde los productos 835 */ 836 function actualizarCheckboxesTipoDesdeProductos(productos) { 837 const tiposDisponibles = new Set(); 838 839 productos.forEach(function (producto) { 840 const tipoField = producto.tipo_bomba || producto.tipo || producto.type || producto.categoria; 841 if (tipoField) { 842 if (Array.isArray(tipoField)) {
843 tipoField.forEach(function (tipo) { 844 const v = (typeof tipo === 'object' && tipo !== null) ? (tipo.slug || tipo.value || tipo.name) : tipo; 845 if (v) tiposDisponibles.add(v); 846 }); 847 } else { 848 const v = (typeof tipoField === 'object' && tipoField !== null) ? (tipoField.slug || tipoField.value || tipoField.name) : tipoField; 849 if (v) tiposDisponibles.add(v); 850 } 851 } 852 }); 853 854 actualizarCheckboxesTipo(Array.from(tiposDisponibles)); 855 } 856 857 function guardarSeleccionTipos() { 858 const seleccionActual = []; 859 $('#filtro-tipo-container input[name="tipo_bomba[]"]:checked').not('#tipo_todos').each(function () { 860 seleccionActual.push($(this).val()); 861 }); 862 sessionStorage.setItem('koslan_tipos_seleccionados', JSON.stringify(seleccionActual)); 863 console.log('ð¾ Tipos guardados en sessionStorage:', seleccionActual); 864 } 865 866 function restaurarSeleccionTipos() { 867 try { 868 const seleccionGuardada = sessionStorage.getItem('koslan_tipos_seleccionados'); 869 if (seleccionGuardada) { 870 const tipos = JSON.parse(seleccionGuardada); 871 console.log('ð¥ Restaurando tipos desde sessionStorage:', tipos); 872 873 tipos.forEach(function (tipoSlug) { 874 const tipoSlugSafe = normalizarSlugParaID(tipoSlug); 875 $('#tipo_' + tipoSlugSafe).prop('checked', true); 876 }); 877 878 if (tipos.length > 0) { 879 $('#tipo_todos').prop('checked', false); 880 } 881 } 882 } catch (error) { 883 console.error('Error al restaurar selección de tipos:', error); 884 } 885 } 886 887 // ======================================== 888 // EVENTOS: Guardar selección al cambiar 889 // ======================================== 890 891 $(document).on('change', 'input[name="tipo_bomba[]"]', function () { 892 guardarSeleccionTipos(); 893 }); 894 895 // Al hacer clic en "Aplicar filtros" 896 $(document).on('click', '#btn-aplicar-filtros', function () { 897 guardarSeleccionTipos(); 898 }); 899 900 // ======================================== 901 // ACTUALIZAR CHECKBOXES SIN BORRAR 902 // ======================================== 903 function actualizarCheckboxesTipo(tiposArray, preservarSeleccion = true) { 904 const $container = $('#filtro-tipo-container'); 905 if (!$container.length) return; 906 907 // Si no hay tipos para mostrar, salir 908 if (!tiposArray || !tiposArray.length) { 909 console.warn('â ï¸ No hay tipos para mostrar'); 910 return; 911 } 912 913 console.log('ð Tipos recibidos de la API:', tiposArray); 914 915 // â Usar los tipos del array (de la API), no los originales guardados 916 // Esto permite que cada búsqueda muestre los tipos relevantes 917 const tiposAMostrar = tiposArray; 918 919 const $todosTipo = $('#tipo_todos'); 920 921 // â FIX: Solo guardar selección si vamos a preservarla 922 const seleccionActual = []; 923 if (preservarSeleccion) { 924 $container.find('input[name="tipo_bomba[]"]:checked').not('#tipo_todos').each(function () { 925 seleccionActual.push($(this).val()); 926 }); 927 } 928 929 const todosChecked = $todosTipo.is(':checked'); 930 931 // Verificar qué tipos ya existen en el DOM 932 const tiposExistentes = new Set(); 933 $container.find('.filtro-checkbox').not(':first').each(function () { 934 const slug = $(this).find('input').val(); 935 tiposExistentes.add(slug); 936 }); 937 938 // Agregar solo los tipos que NO existen 939 tiposAMostrar.forEach(function (tipo) { 940 let tipoSlug, tipoNombre; 941 942 if (typeof tipo === 'object' && tipo !== null) { 943 tipoSlug = tipo.slug || tipo.value || String(tipo); 944 tipoNombre = tipo.nombre || tipo.name || tipo.label || tipo.title || capitalizeWords(tipoSlug); 945 } else { 946 tipoSlug = String(tipo); 947 tipoNombre = capitalizeWords(tipo); 948 } 949 950 const tipoSlugSafe = normalizarSlugParaID(tipoSlug); 951 952 // â FIX: Solo agregar checkboxes que NO existen 953 // NO modificar el estado de checkboxes existentes para no sobrescribir selección del usuario 954 if (!tiposExistentes.has(tipoSlug)) { 955 // Si NO existe, agregarlo (sin checked por defecto) 956 const checkboxHtml = 957 '<div class="filtro-checkbox">' + 958 '<input type="checkbox" id="tipo_' + tipoSlugSafe + '" name="tipo_bomba[]" value="' + tipoSlug + '">' + 959 '<label for="tipo_' + tipoSlugSafe + '">' + tipoNombre + '</label>' + 960 '</div>'; 961 $container.append(checkboxHtml); 962 console.log('â Tipo agregado:', tipoNombre); 963 } 964 // â FIX: Ya NO modificamos checkboxes existentes - la selección del usuario se preserva 965 }); 966 967 // â Guardar tipos originales DESPUÃS de agregarlos (solo si está vacÃo) 968 if (window.__TIPOS_ORIGINALES__.length === 0) { 969 $container.find('.filtro-checkbox').not(':first').each(function () { 970 const checkbox = $(this).find('input[type="checkbox"]'); 971 const tipo = { 972 slug: checkbox.val(), 973 nombre: $(this).find('label').text().trim() 974 }; 975 window.__TIPOS_ORIGINALES__.push(tipo); 976 }); 977 console.log('ð¾ Tipos originales guardados:', window.__TIPOS_ORIGINALES__); 978 } 979 980 // â FIX: No modificar el estado de "Todos" después de búsquedas 981 // El usuario ya controló este estado manualmente 982 983 bindTipoCheckboxEvents(); 984 } 985 986 /** 987 * Actualiza los checkboxes de marca desde los productos (MOBILE) 988 */ 989 function actualizarCheckboxesMarcaDesdeProductos(productos) { 990 const marcasDisponibles = new Set(); 991 992 productos.forEach(function (producto) { 993 const marcaField = producto.marca || producto.brand; 994 if (marcaField) { 995 if (Array.isArray(marcaField)) { 996 marcaField.forEach(function (marca) { 997 const v = (typeof marca === 'object' && marca !== null) 998 ? (marca.slug || marca.value || marca.name) 999 : marca; 1000 if (v) marcasDisponibles.add(v); 1001 }); 1002 } else { 1003 const v = (typeof marcaField === 'object' && marcaField !== null) 1004 ? (marcaField.slug || marcaField.value || marcaField.name) 1005 : marcaField; 1006 if (v) marcasDisponibles.add(v); 1007 } 1008 } 1009 }); 1010 1011 actualizarCheckboxesMarca(Array.from(marcasDisponibles)); 1012 } 1013 1014 /** 1015 * Actualiza los checkboxes de marca dinámicamente (MOBILE) 1016 */ 1017 function actualizarCheckboxesMarca(marcasArray) { 1018 const $container = $('#filtro-marca-container'); 1019 if (!$container.length) return; 1020 1021 const $todosMarca = $('#marca_todas'); 1022 const todosChecked = $todosMarca.is(':checked'); 1023 1024 // Limpiar checkboxes excepto "Todas" 1025 $container.find('.filtro-checkbox').not(':first').remove(); 1026 1027 marcasArray.forEach(function (marca) { 1028 let marcaSlug, marcaNombre; 1029 1030 if (typeof marca === 'object' && marca !== null) { 1031 marcaSlug = marca.slug || marca.value || String(marca); 1032 marcaNombre = marca.name || marca.label || marca.title || capitalizeWords(marcaSlug); 1033 } else { 1034 marcaSlug = String(marca); 1035 marcaNombre = capitalizeWords(marca); 1036 } 1037 1038 // â IMPORTANTE: Normalizar el slug para el ID (sin caracteres especiales) 1039 const marcaSlugSafe = normalizarSlugParaID(marcaSlug); 1040 1041 const checkboxHtml = 1042 '<div class="filtro-checkbox" data-marca-slug="' + marcaSlug + '">' + 1043 '<input type="checkbox" id="marca_' + marcaSlugSafe + '" name="marca_checkbox[]" value="' + marcaSlug + '">' + 1044 '<label for="marca_' + marcaSlugSafe + '">' + marcaNombre + '</label>' + 1045 '</div>'; 1046 $container.append(checkboxHtml); 1047 }); 1048 1049 $todosMarca.prop('checked', todosChecked); 1050 bindMarcaCheckboxEvents(); 1051 } 1052 1053 /** 1054 * Eventos para los checkboxes de marca (similar a tipo y voltaje) 1055 */ 1056 function bindMarcaCheckboxEvents() { 1057 $('#marca_todas, #marca_todas_mobile').off('change.marcas'); 1058 $('input[name="marca_checkbox[]"]').not('#marca_todas, #marca_todas_mobile').off('change.marcas'); 1059 1060 $('#marca_todas, #marca_todas_mobile').on('change.marcas', function () { 1061 if ($(this).is(':checked')) { 1062 const container = $(this).closest('.filtro-checkbox-group'); 1063 container.find('input[name="marca_checkbox[]"]').not('#marca_todas, #marca_todas_mobile').prop('checked', false); 1064 } 1065 }); 1066 1067 $('input[name="marca_checkbox[]"]').not('#marca_todas, #marca_todas_mobile').on('change.marcas', function () { 1068 if ($(this).is(':checked')) { 1069 const container = $(this).closest('.filtro-checkbox-group'); 1070 container.find('#marca_todas, #marca_todas_mobile').prop('checked', false); 1071 } 1072 1073 const container = $(this).closest('.filtro-checkbox-group'); 1074 const checkedCount = container.find('input[name="marca_checkbox[]"]:checked').not('#marca_todas, #marca_todas_mobile').length; 1075 1076 if (checkedCount === 0) { 1077 container.find('#marca_todas, #marca_todas_mobile').prop('checked', true); 1078 } 1079 }); 1080 } 1081 1082 function capitalizeWords(str) { 1083 if (!str) return ''; 1084 return String(str).replace(/[-_]/g, ' ') 1085 .split(' ') 1086 .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) 1087 .join(' '); 1088 } 1089 1090 function bindTipoCheckboxEvents() {
1091 $('#tipo_todos').off('change.tipoBomba'); 1092 $('input[name="tipo_bomba[]"]').not('#tipo_todos').off('change.tipoBomba'); 1093 1094 $('#tipo_todos').on('change.tipoBomba', function () { 1095 if ($(this).is(':checked')) { 1096 $('input[name="tipo_bomba[]"]').not('#tipo_todos').prop('checked', false); 1097 // â FIX: Resetear filtros de precio y potencia cuando se selecciona "Todos" 1098 resetearFiltrosPrecioPotencia(); 1099 1100 // â FIX CRÃTICO: Ejecutar búsqueda automáticamente para actualizar marcas 1101 if ($('#resultados-bombas').is(':visible')) { 1102 console.log('ð Tipo cambiado a "Todos" - Re-ejecutando búsqueda...'); 1103 setTimeout(function() { 1104 buscarBombas(false); 1105 }, 300); 1106 } 1107 } 1108 }); 1109 $('input[name="tipo_bomba[]"]').not('#tipo_todos').on('change.tipoBomba', function () { 1110 if ($(this).is(':checked')) $('#tipo_todos').prop('checked', false); 1111 if ($('input[name="tipo_bomba[]"]:checked').not('#tipo_todos').length === 0) $('#tipo_todos').prop('checked', true); 1112 1113 // â Resetear filtros de precio y potencia cuando cambia el tipo de bomba 1114 resetearFiltrosPrecioPotencia(); 1115 1116 // â CRÃTICO: Limpiar sessionStorage para evitar que se carguen valores de tipo anterior 1117 if (typeof limpiarStorage === 'function') { 1118 limpiarStorage(); 1119 console.log('ðï¸ SessionStorage limpiado al cambiar tipo de bomba'); 1120 } 1121 1122 // â REMOVIDO: Auto-trigger - Ahora es TODO MANUAL 1123 // El usuario debe hacer clic en "Aplicar filtros" para ejecutar búsqueda 1124 console.log('ð Tipo de bomba cambiado - Esperando "Aplicar filtros"'); 1125 }); 1126 } 1127 1128 /** 1129 * Resetea los filtros de precio y potencia a sus valores por defecto 1130 * Se llama cuando el usuario cambia el tipo de bomba 1131 */ 1132 function resetearFiltrosPrecioPotencia() { 1133 console.log('ð Reseteando filtros de precio y potencia por cambio de tipo...'); 1134 1135 // Limpiar valores de los inputs hidden 1136 $('#precio_min').val(''); 1137 $('#precio_max').val(''); 1138 $('#potencia_min').val(''); 1139 $('#potencia_max').val(''); 1140 1141 // â NO resetear los sliders aquà - se resetearán cuando llegue la respuesta del servidor 1142 // con los rangos correctos del nuevo tipo de bomba 1143 // Los sliders se actualizarán en actualizarRangosSliders() con los rangos del servidor 1144 1145 console.log('â Filtros de precio/potencia limpiados - Esperando rangos del servidor'); 1146 } 1147 1148 /** 1149 * Actualiza el rango de precio con nuevos valores min/max 1150 */ 1151 function actualizarRangoPrecio(min, max) { 1152 const minRedondeado = Math.floor(min); 1153 const maxRedondeado = Math.ceil(max); 1154 1155 console.log('ð° Actualizando rango precio:', minRedondeado, '-', maxRedondeado); 1156 1157 RANGO_PRECIO = { 1158 min: minRedondeado, 1159 max: maxRedondeado, 1160 step: 1000 1161 }; 1162 1163 $('#precio_min').attr({ 1164 placeholder: 'MÃn: ' + minRedondeado.toLocaleString('es-CL'), 1165 min: minRedondeado, 1166 max: maxRedondeado 1167 }); 1168 $('#precio_max').attr({ 1169 placeholder: 'Máx: ' + maxRedondeado.toLocaleString('es-CL'), 1170 max: maxRedondeado, 1171 min: minRedondeado 1172 }); 1173 1174 const $precioSlider = $('#precio-slider'); 1175 if ($precioSlider.length && $precioSlider.data('ionRangeSlider')) { 1176 try { 1177 $precioSlider.data('ionRangeSlider').update({ 1178 min: minRedondeado, 1179 max: maxRedondeado, 1180 from: minRedondeado, 1181 to: maxRedondeado 1182 }); 1183 console.log('â Slider de precio actualizado directamente'); 1184 } catch (e) { 1185 console.error('Error al actualizar slider de precio:', e); 1186 } 1187 } 1188 1189 if (typeof window.actualizarRangosSliders === 'function') { 1190 window.actualizarRangosSliders({ 1191 precio_min: minRedondeado, 1192 precio_max: maxRedondeado, 1193 potencia_min: RANGO_POTENCIA.min, 1194 potencia_max: RANGO_POTENCIA.max 1195 }); 1196 } 1197 } 1198 1199 /** 1200 * Actualiza el rango de potencia con nuevos valores min/max 1201 */ 1202 function actualizarRangoPotencia(min, max) { 1203 const minRedondeado = Math.floor(min * 10) / 10; 1204 const maxRedondeado = Math.ceil(max * 10) / 10; 1205 1206 console.log('â¡ Actualizando rango potencia:', minRedondeado, '-', maxRedondeado); 1207 1208 RANGO_POTENCIA = { 1209 min: minRedondeado, 1210 max: maxRedondeado, 1211 step: 0.1 1212 }; 1213 1214 $('#potencia_min').attr({ 1215 placeholder: 'MÃn: ' + minRedondeado + ' HP', 1216 min: minRedondeado, 1217 max: maxRedondeado 1218 }); 1219 $('#potencia_max').attr({ 1220 placeholder: 'Máx: ' + maxRedondeado + ' HP', 1221 max: maxRedondeado, 1222 min: minRedondeado 1223 }); 1224 1225 const $potenciaSlider = $('#potencia-slider'); 1226 if ($potenciaSlider.length && $potenciaSlider.data('ionRangeSlider')) { 1227 try { 1228 $potenciaSlider.data('ionRangeSlider').update({ 1229 min: minRedondeado, 1230 max: maxRedondeado, 1231 from: minRedondeado, 1232 to: maxRedondeado 1233 }); 1234 console.log('â Slider de potencia actualizado directamente'); 1235 } catch (e) { 1236 console.error('Error al actualizar slider de potencia:', e); 1237 } 1238 } 1239 1240 if (typeof window.actualizarRangosSliders === 'function') { 1241 window.actualizarRangosSliders({ 1242 precio_min: RANGO_PRECIO.min, 1243 precio_max: RANGO_PRECIO.max, 1244 potencia_min: minRedondeado, 1245 potencia_max: maxRedondeado 1246 }); 1247 } 1248 } 1249 1250 /** 1251 * Inicializa Select2 en el select de marcas (DESKTOP) 1252 */ 1253 function initSelect2Marca() { 1254 const $select = $('#select-marca'); 1255 1256 // No existe â no hacemos nada 1257 if (!$select.length) return; 1258 1259 // â No inicializar Select2 si estamos en mobile (< 992px) 1260 if ($(window).width() <= 991) { 1261 console.log('ð± Mobile: Select2 no se inicializa'); 1262 return; 1263 } 1264 1265 // Ya está inicializado â no repetir 1266 if ($select.hasClass('select2-hidden-accessible')) { 1267 console.log('â¹ï¸ Select2 ya está inicializado en desktop'); 1268 return; 1269 } 1270 1271 if (typeof $.fn.select2 === 'undefined') { 1272 console.error('â Select2 no está disponible'); 1273 return; 1274 } 1275 1276 try { 1277 $select.select2({ 1278 placeholder: 'Seleccionar marcas', 1279 allowClear: true, 1280 width: '100%', 1281 language: { 1282 noResults: () => "No se encontraron marcas", 1283 searching: () => "Buscando..." 1284 } 1285 }); 1286 1287 console.log('ð¥ï¸ Select2 inicializado (desktop)'); 1288 } catch (err) { 1289 console.error('â Error al inicializar Select2:', err); 1290 } 1291 } 1292 1293 function sincronizarTolerancias(origen) { 1294 const esMobile = String(origen).indexOf('_mobile') !== -1; 1295 1296 if (esMobile) { 1297 const valorCaudal = $('#tolerancia_caudal_mobile').val() || '40'; 1298 const valorPresion = $('#tolerancia_presion_mobile').val() || '40'; 1299 1300 $('#tolerancia_caudal').val(valorCaudal); 1301 $('#tolerancia_presion').val(valorPresion); 1302 } else {
1303 const valorCaudal = $('#tolerancia_caudal').val() || '40'; 1304 const valorPresion = $('#tolerancia_presion').val() || '40'; 1305 1306 $('#tolerancia_caudal_mobile').val(valorCaudal); 1307 $('#tolerancia_presion_mobile').val(valorPresion); 1308 } 1309 1310 actualizarLabelsTolerancia(); 1311 } 1312 1313 function actualizarLabelsTolerancia() { 1314 const valorCaudal = $('#tolerancia_caudal').val() || $('#tolerancia_caudal_mobile').val() || '40'; 1315 const valorPresion = $('#tolerancia_presion').val() || $('#tolerancia_presion_mobile').val() || '40'; 1316 1317 $('#tolerancia_caudal_value, #tolerancia_caudal_value_mobile').text(valorCaudal); 1318 $('#tolerancia_presion_value, #tolerancia_presion_value_mobile').text(valorPresion); 1319 } 1320 1321 /** 1322 * Resetea visualmente todos los filtros sin disparar una nueva búsqueda 1323 * Se usa cuando se hace una nueva búsqueda principal con nuevos valores de caudal/presión 1324 */ 1325 function resetearFiltrosVisualmente() { 1326 console.log('ð Reseteando filtros visualmente para nueva búsqueda...'); 1327 1328 // 1. Resetear checkboxes de tipo 1329 $('#tipo_todos').prop('checked', true); 1330 $('input[name="tipo_bomba[]"]').not('#tipo_todos').prop('checked', false); 1331 1332 // 2. Resetear checkboxes de voltaje 1333 $('#voltaje_todos').prop('checked', true); 1334 $('input[name="voltaje[]"]').not('#voltaje_todos').prop('checked', false); 1335 1336 // 3. Resetear inputs de precio y potencia 1337 $('#precio_min, #precio_max, #potencia_min, #potencia_max').val(''); 1338 1339 // 4. Resetear sliders de precio a sus rangos completos 1340 const $precioSlider = $('#precio-slider'); 1341 if ($precioSlider.length && $precioSlider.data('ionRangeSlider')) { 1342 const sliderData = $precioSlider.data('ionRangeSlider'); 1343 sliderData.update({ 1344 from: sliderData.options.min, 1345 to: sliderData.options.max 1346 }); 1347 } 1348 1349 // 5. Resetear sliders de potencia a sus rangos completos 1350 const $potenciaSlider = $('#potencia-slider'); 1351 if ($potenciaSlider.length && $potenciaSlider.data('ionRangeSlider')) { 1352 const sliderData = $potenciaSlider.data('ionRangeSlider'); 1353 sliderData.update({ 1354 from: sliderData.options.min, 1355 to: sliderData.options.max 1356 }); 1357 } 1358 1359 // 6. Resetear Select2 de marcas (desktop) 1360 const $selectMarca = $('#select-marca'); 1361 if ($selectMarca.length) { 1362 if ($selectMarca.hasClass('select2-hidden-accessible')) { 1363 $selectMarca.val(null).trigger('change'); 1364 } else { 1365 $selectMarca.val([]); 1366 } 1367 } 1368 1369 // 7. Resetear checkboxes de marca (mobile) 1370 $('#marca_todas, #marca_todas_mobile').prop('checked', true); 1371 $('input[name="marca_checkbox[]"]').not('#marca_todas, #marca_todas_mobile').prop('checked', false); 1372 1373 // 8. Resetear tolerancias a valores por defecto 1374 $('#tolerancia_caudal').val(40); 1375 $('#tolerancia_presion').val(40); 1376 actualizarLabelsTolerancia(); 1377 1378 // 9. Resetear ordenamiento 1379 $('#select-ordenar').val('mejor_opcion'); 1380 1381 // 10. Limpiar sessionStorage de tipos 1382 sessionStorage.removeItem('koslan_tipos_seleccionados'); 1383 1384 // 11. Limpiar el offcanvas mobile si estaba clonado 1385 limpiarOffcanvasMobile(); 1386 1387 // 12. â IMPORTANTE: Limpiar tipos originales para que se regeneren con la nueva búsqueda 1388 window.__TIPOS_ORIGINALES__ = []; 1389 1390 // 13. Limpiar checkboxes de tipo existentes (excepto "Todos") para regenerar 1391 $('#filtro-tipo-container .filtro-checkbox').not(':first').remove(); 1392 1393 console.log('â Filtros reseteados visualmente'); 1394 } 1395 1396 function buscarBombas(usarDefecto = false) { 1397 let caudal = parseFloat($('#input_caudal').val()) || currentCaudal; 1398 const presion = parseFloat($('#input_presion').val()) || currentPresion; 1399 1400 if (!caudal || !presion) { 1401 alert('Por favor, ingresa valores de caudal y presión o usa las calculadoras.'); 1402 return; 1403 } 1404 1405 const unit = $('#flowRateUnit').val() || 'lmin'; 1406 let caudalEnLMin = caudal; 1407 if (unit === 'ls') caudalEnLMin = caudal * 60; 1408 if (unit === 'm3h') caudalEnLMin = caudal * 16.667; 1409 1410 const toleranciaCaudal = parseInt($('#tolerancia_caudal').val()) || 40; 1411 const toleranciaPresion = parseInt($('#tolerancia_presion').val()) || 40; 1412 1413 // â Mantener labels sincronizados antes del request 1414 actualizarLabelsTolerancia(); 1415 1416 // â NUEVO: Detectar si cambiaron caudal, presión o tolerancias 1417 const caudalCambio = Math.abs(caudalEnLMin - busquedaOriginal.caudal) > 0.1; 1418 const presionCambio = Math.abs(presion - busquedaOriginal.presion) > 0.1; 1419 const toleranciaCaudalCambio = toleranciaCaudal !== busquedaOriginal.toleranciaCaudal; 1420 const toleranciaPresionCambio = toleranciaPresion !== busquedaOriginal.toleranciaPresion; 1421 1422 const parametrosPrincipalesCambiaron = caudalCambio || presionCambio || toleranciaCaudalCambio || toleranciaPresionCambio; 1423 1424 // â Variable para saber si es búsqueda base (debe actualizar rangos) 1425 let esBusquedaBase = usarDefecto || parametrosPrincipalesCambiaron; 1426 1427 // â Si es búsqueda nueva O cambiaron parámetros principales, actualizar búsqueda original 1428 if (esBusquedaBase) { 1429 console.log('ð BÃSQUEDA BASE:', usarDefecto ? 'Nueva búsqueda' : 'Parámetros principales cambiaron'); 1430 1431 busquedaOriginal = { 1432 caudal: caudalEnLMin, 1433 presion: presion, 1434 toleranciaCaudal: toleranciaCaudal, 1435 toleranciaPresion: toleranciaPresion, 1436 activa: true 1437 }; 1438 1439 resetearFiltrosVisualmente(); 1440 1441 // â Restaurar los valores reales de la búsqueda base después del reset visual 1442 $('#tolerancia_caudal').val(toleranciaCaudal); 1443 $('#tolerancia_presion').val(toleranciaPresion); 1444 actualizarLabelsTolerancia(); 1445 } else if (busquedaOriginal.activa) { 1446 // â Usar parámetros de búsqueda original (solo cambiaron filtros secundarios) 1447 console.log('ð FILTRANDO sobre búsqueda original:', { 1448 caudal: busquedaOriginal.caudal, 1449 presion: busquedaOriginal.presion, 1450 toleranciaCaudal: busquedaOriginal.toleranciaCaudal, 1451 toleranciaPresion: busquedaOriginal.toleranciaPresion 1452 }); 1453 1454 // â Asegurar que la UI refleje la búsqueda original cuando solo se filtra 1455 $('#tolerancia_caudal').val(busquedaOriginal.toleranciaCaudal); 1456 $('#tolerancia_presion').val(busquedaOriginal.toleranciaPresion); 1457 actualizarLabelsTolerancia(); 1458 } 1459 1460 const filtros = { 1461 action: 'buscar_bombas_koslan', 1462 caudal: busquedaOriginal.activa ? busquedaOriginal.caudal : caudalEnLMin, 1463 presion: busquedaOriginal.activa ? busquedaOriginal.presion : presion, 1464 tolerancia_caudal: busquedaOriginal.activa ? busquedaOriginal.toleranciaCaudal : toleranciaCaudal, 1465 tolerancia_presion: busquedaOriginal.activa ? busquedaOriginal.toleranciaPresion : toleranciaPresion, 1466 paged: currentPage, 1467 ordenar: usarDefecto ? 'mejor_opcion' : ($('#select-ordenar').val() || 'mejor_opcion'), 1468 'voltaje[]': ['todos'], 1469 precio_min: '', 1470 precio_max: '', 1471 potencia_min: '', 1472 potencia_max: '' 1473 }; 1474 1475 if (typeof calcKoslanAjax !== 'undefined' && calcKoslanAjax.nonce) { 1476 filtros.nonce = calcKoslanAjax.nonce; 1477 } else if (typeof ajax_params !== 'undefined' && ajax_params.nonce) { 1478 filtros.nonce = ajax_params.nonce; 1479 } 1480 1481 // â Solo aplicar filtros secundarios si NO se solicitan valores por defecto 1482 if (!usarDefecto) {
1483 // Filtro de voltaje - SOLO del desktop (los mobile se sincronizan a desktop) 1484 const voltsMarcados = $('#filtros-desktop input[name="voltaje[]"]:checked').not('#voltaje_todos').map(function () { 1485 return $(this).val(); 1486 }).get(); 1487 1488 if (voltsMarcados.length > 0) { 1489 filtros['voltaje[]'] = voltsMarcados; 1490 } 1491 1492 // â Filtro de precio - solo enviar si usuario los modificó manualmente 1493 const pmin = parseFloat($('#precio_min').val()); 1494 const pmax = parseFloat($('#precio_max').val()); 1495 1496 if (!esBusquedaBase && rangosOriginales.guardados) { 1497 const precioMinEsOriginal = isNaN(pmin) || Math.abs(pmin - rangosOriginales.precioMin) < 1; 1498 const precioMaxEsOriginal = isNaN(pmax) || Math.abs(pmax - rangosOriginales.precioMax) < 1; 1499 1500 if (!precioMinEsOriginal) filtros.precio_min = pmin; 1501 if (!precioMaxEsOriginal) filtros.precio_max = pmax; 1502 } else { 1503 if (!isNaN(pmin)) filtros.precio_min = pmin; 1504 if (!isNaN(pmax)) filtros.precio_max = pmax; 1505 } 1506 1507 // â Filtro de potencia 1508 const potmin = parseFloat($('#potencia_min').val()); 1509 const potmax = parseFloat($('#potencia_max').val()); 1510 1511 if (!esBusquedaBase && rangosOriginales.guardados) { 1512 const potenciaMinEsOriginal = isNaN(potmin) || Math.abs(potmin - rangosOriginales.potenciaMin) < 0.1; 1513 const potenciaMaxEsOriginal = isNaN(potmax) || Math.abs(potmax - rangosOriginales.potenciaMax) < 0.1; 1514 1515 if (!potenciaMinEsOriginal) filtros.potencia_min = potmin; 1516 if (!potenciaMaxEsOriginal) filtros.potencia_max = potmax; 1517 } else { 1518 if (!isNaN(potmin)) filtros.potencia_min = potmin; 1519 if (!isNaN(potmax)) filtros.potencia_max = potmax; 1520 } 1521 } 1522 1523 // â Antes de enviar, asegurar una vez más el texto visible 1524 actualizarLabelsTolerancia(); 1525 1526 console.log('ð¤ Enviando filtros:', filtros); 1527 1528 const ajaxUrl = 1529 (typeof calcKoslanAjax !== 'undefined' && calcKoslanAjax.ajax_url) 1530 ? calcKoslanAjax.ajax_url 1531 : ((typeof calcKoslanAjax !== 'undefined' && calcKoslanAjax.ajaxurl) 1532 ? calcKoslanAjax.ajaxurl 1533 : ((typeof ajax_params !== 'undefined' && ajax_params.ajax_url) 1534 ? ajax_params.ajax_url 1535 : ((typeof ajax_params !== 'undefined' && ajax_params.ajaxurl) 1536 ? ajax_params.ajaxurl 1537 : ((typeof ajaxurl !== 'undefined') 1538 ? ajaxurl 1539 : '/wp-admin/admin-ajax.php')))); 1540 1541 $.ajax({ 1542 url: ajaxUrl, 1543 type: 'POST', 1544 data: filtros, 1545 dataType: 'json', 1546 beforeSend: function () { 1547 $('#btn-aplicar-filtros').prop('disabled', true).text('Aplicando...'); 1548 }, 1549 success: function (response) { 1550 console.log('ð¦ Respuesta recibida:', response); 1551 1552 $('#ayuda').hide(); 1553 hideLoading(); 1554 1555 if (response.success) { 1556 $('#bombas-grid').html(response.data.html); 1557 const total = response.data.total || 0; 1558 1559 let infoText = '<strong>Bombas recomendadas (' + total + ' encontrada' + (total !== 1 ? 's' : '') + ')</strong>'; 1560 const caudal = parseFloat($('#input_caudal').val()) || currentCaudal; 1561 if (unit !== 'lmin') { 1562 infoText += '<br><small class="text-muted">Búsqueda realizada con caudal: ' + caudalEnLMin.toFixed(1) + ' l/min'; 1563 if (unit === 'ls') infoText += ' (equiv. ' + caudal + ' l/s)'; 1564 if (unit === 'm3h') infoText += ' (equiv. ' + caudal + ' m³/h)'; 1565 infoText += '</small>'; 1566 } 1567 $('#resultados-info').html(infoText); 1568 1569 if (response.data.pagination) $('#pagination-container').html(response.data.pagination); 1570 else renderPagination(response.data.current_page || 1, response.data.max_pages || 1); 1571 1572 // â NUEVO: Solo actualizar rangos si es búsqueda base 1573 if (response.data.precio_min !== undefined && response.data.precio_max !== undefined) { 1574 if (esBusquedaBase) { 1575 // Es búsqueda base: actualizar rangos y guardarlos como originales 1576 console.log('ð° [BÃSQUEDA BASE] Actualizando rangos de precio:', { 1577 min: response.data.precio_min, 1578 max: response.data.precio_max 1579 }); 1580 1581 // Guardar como rangos originales 1582 rangosOriginales.precioMin = response.data.precio_min; 1583 rangosOriginales.precioMax = response.data.precio_max; 1584 rangosOriginales.guardados = true; 1585 1586 actualizarRangoPrecio(response.data.precio_min, response.data.precio_max); 1587 1588 setTimeout(function () { 1589 const sliderInstance = $('#precio-slider').data('ionRangeSlider'); 1590 if (sliderInstance) { 1591 console.log('â Slider precio actualizado:', { 1592 min: sliderInstance.old_min, 1593 max: sliderInstance.old_max 1594 }); 1595 } 1596 }, 100); 1597 } else if (rangosOriginales.guardados) { 1598 // Es filtro secundario: restaurar rangos originales 1599 console.log('ð [FILTRO SECUNDARIO] Restaurando rangos originales de precio:', { 1600 min: rangosOriginales.precioMin, 1601 max: rangosOriginales.precioMax 1602 }); 1603 1604 actualizarRangoPrecio(rangosOriginales.precioMin, rangosOriginales.precioMax); 1605 } 1606 } else { 1607 console.warn('â ï¸ El servidor no devolvió precio_min o precio_max'); 1608 } 1609 1610 if (response.data.potencia_min !== undefined && response.data.potencia_max !== undefined) { 1611 if (esBusquedaBase) { 1612 // Es búsqueda base: actualizar rangos y guardarlos como originales 1613 console.log('â¡ [BÃSQUEDA BASE] Actualizando rangos de potencia:', { 1614 min: response.data.potencia_min, 1615 max: response.data.potencia_max 1616 }); 1617 1618 // Guardar como rangos originales 1619 rangosOriginales.potenciaMin = response.data.potencia_min; 1620 rangosOriginales.potenciaMax = response.data.potencia_max; 1621 1622 actualizarRangoPotencia(response.data.potencia_min, response.data.potencia_max); 1623 1624 setTimeout(function () { 1625 const sliderInstance = $('#potencia-slider').data('ionRangeSlider'); 1626 if (sliderInstance) { 1627 console.log('â Slider potencia actualizado:', { 1628 min: sliderInstance.old_min, 1629 max: sliderInstance.old_max 1630 }); 1631 } 1632 }, 100); 1633 } else if (rangosOriginales.guardados) { 1634 // Es filtro secundario: restaurar rangos originales 1635 console.log('ð [FILTRO SECUNDARIO] Restaurando rangos originales de potencia:', { 1636 min: rangosOriginales.potenciaMin, 1637 max: rangosOriginales.potenciaMax 1638 }); 1639 1640 actualizarRangoPotencia(rangosOriginales.potenciaMin, rangosOriginales.potenciaMax); 1641 } 1642 } else { 1643 console.warn('â ï¸ El servidor no devolvió potencia_min o potencia_max'); 1644 } 1645 1646 // â Actualizar marcas Select2 (desktop) y checkboxes (mobile) con visibilidad 1647 if (response.data.marcas_disponibles && Array.isArray(response.data.marcas_disponibles)) { 1648 window.__ULTIMAS_MARCAS_DISPONIBLES__ = response.data.marcas_disponibles; 1649 1650 const seleccionApi = Array.isArray(response.data.marca_seleccionad
1650as) 1651 ? response.data.marca_seleccionadas 1652 : null; 1653 1654 actualizarSelectMarcasAPI(response.data.marcas_disponibles, seleccionApi); 1655 } 1656 1657 // â Actualizar tipos (ahora no borra los existentes) 1658 if (response.data.productos && Array.isArray(response.data.productos)) { 1659 actualizarCheckboxesTipoDesdeProductos(response.data.productos); 1660 } 1661 1662 // â Re-inicializar Select2 1663 setTimeout(initSelect2Marca, 300); 1664 1665 $('#resultados-bombas').removeClass('d-none'); 1666 1667 if (currentPage === 1) { 1668 $('html, body').animate({ 1669 scrollTop: $('#resultados-bombas').offset().top - 100 1670 }, 500); 1671 } 1672 } else { 1673 const mensaje = response.data && response.data.message ? response.data.message : 'No se encontraron resultados con los filtros aplicados.'; 1674 $('#bombas-grid').html( 1675 '<div class="alert alert-warning" style="padding: 40px; text-align: center;">' + 1676 '<h4>Sin resultados</h4>' + 1677 '<p>' + mensaje + '</p>' + 1678 '<button class="btn btn-primary" id="btn-resetear-filtros">Limpiar filtros</button>' + 1679 '</div>' 1680 ); 1681 $('#resultados-bombas').removeClass('d-none'); 1682 } 1683 }, 1684 error: function (xhr, status, error) { 1685 console.error('ERROR AJAX:', status, error, xhr.responseText); 1686 hideLoading(); 1687 1688 // Manejo especÃfico de error 403 (nonce expirado) 1689 if (xhr.status === 403) { 1690 console.warn('â ï¸ Error 403 detectado - Probable nonce expirado'); 1691 $('#bombas-grid').html( 1692 '<div class="alert alert-warning" style="padding: 40px; text-align: center;">' + 1693 '<h4>â° Sesión expirada</h4>' + 1694 '<p>Tu sesión ha expirado por inactividad. Por favor, recarga la página para continuar.</p>' + 1695 '<button class="btn btn-primary" onclick="location.reload()">ð Recargar página</button>' + 1696 '</div>' 1697 ); 1698 } else { 1699 $('#bombas-grid').html( 1700 '<div class="alert alert-danger" style="padding: 40px; text-align: center;">' + 1701 '<h4>Error al cargar resultados</h4>' + 1702 '<p>Hubo un problema al procesar su solicitud. Por favor, intente nuevamente.</p>' + 1703 '<p><small>Error: ' + error + ' (Status: ' + xhr.status + ')</small></p>' + 1704 '<button class="btn btn-primary" onclick="location.reload()">Recargar página</button>' + 1705 '</div>' 1706 ); 1707 } 1708 $('#resultados-bombas').removeClass('d-none'); 1709 }, 1710 complete: function () { 1711 $('#btn-aplicar-filtros').prop('disabled', false).text('Aplicar filtros'); 1712 } 1713 }); 1714 } 1715 1716 function renderPagination(current, max) { 1717 if (max <= 1) { 1718 $('#pagination-container').html(''); 1719 return; 1720 } 1721 let html = ''; 1722 html += '<button class="pagination-btn" data-page="' + (current - 1) + '" ' + (current === 1 ? 'disabled' : '') + '>â¹ Anterior</button>'; 1723 let startPage = Math.max(1, current - 2); 1724 let endPage = Math.min(max, current + 2); 1725 if (startPage > 1) { 1726 html += '<button class="pagination-btn" data-page="1">1</button>'; 1727 if (startPage > 2) html += '<span class="pagination-dots">...</span>'; 1728 } 1729 for (let i = startPage; i <= endPage; i++) { 1730 html += '<button class="pagination-btn ' + (i === current ? 'active' : '') + '" data-page="' + i + '">' + i + '</button>'; 1731 } 1732 if (endPage < max) { 1733 if (endPage < max - 1) html += '<span class="pagination-dots">...</span>'; 1734 html += '<button class="pagination-btn" data-page="' + max + '">' + max + '</button>'; 1735 } 1736 html += '<button class="pagination-btn" data-page="' + (current + 1) + '" ' + (current === max ? 'disabled' : '') + '>Siguiente âº</button>'; 1737 $('#pagination-container').html(html); 1738 } 1739 1740 function limpiarFiltros() { 1741 console.log('ð§¹ Limpiando filtros...'); 1742 1743 // 1. Restaurar tipos originales completos 1744 if (window.__TIPOS_ORIGINALES__.length > 0) { 1745 actualizarCheckboxesTipo(window.__TIPOS_ORIGINALES__); 1746 } 1747 1748 // 2. Resetear filtros visualmente (reutiliza la función común) 1749 resetearFiltrosVisualmente(); 1750 1751 // 3. Limpiar clases de validación adicionales 1752 $('#precio_min, #precio_max, #potencia_min, #potencia_max').removeClass('is-invalid')
1753 .next('.invalid-feedback').remove(); 1754 1755 // 4. Llamar a resetearSliders si existe (para sliders personalizados) 1756 if (window.resetearSliders) { 1757 window.resetearSliders(); 1758 } 1759 1760 // 5. Buscar con filtros limpios (NO con usarDefecto para que no resetee de nuevo) 1761 currentPage = 1; 1762 buscarBombas(false); 1763 } 1764 1765 function mostrarMensaje(texto) { 1766 const mensaje = $('<div class="calc-mensaje-exito">' + texto + '</div>'); 1767 $('body').append(mensaje); 1768 mensaje.fadeIn(300).delay(2000).fadeOut(300, function () { 1769 $(this).remove(); 1770 }); 1771 } 1772 1773 function showLoading() { 1774 if ($('#loading-overlay').length === 0) { 1775 $('body').append('<div id="loading-overlay" class="loading-overlay"><div class="loading-spinner"></div></div>'); 1776 } 1777 } 1778 1779 function hideLoading() { 1780 $('#loading-overlay').remove(); 1781 } 1782 1783 function initFiltros() { 1784 $(document) 1785 .off('input.tolerancias change.tolerancias', '#tolerancia_caudal, #tolerancia_presion, #tolerancia_caudal_mobile, #tolerancia_presion_mobile') 1786 .on('input.tolerancias change.tolerancias', '#tolerancia_caudal, #tolerancia_presion, #tolerancia_caudal_mobile, #tolerancia_presion_mobile', function () { 1787 sincronizarTolerancias(this.id); 1788 }); 1789 1790 actualizarLabelsTolerancia(); 1791 1792 bindTipoCheckboxEvents(); 1793 1794 $('#voltaje_todos').on('change', function () { 1795 if ($(this).is(':checked')) { 1796 $('input[name="voltaje[]"]').not('#voltaje_todos').prop('checked', false); 1797 } 1798 }); 1799 1800 $('input[name="voltaje[]"]').not('#voltaje_todos').on('change', function () { 1801 if ($(this).is(':checked')) { 1802 $('#voltaje_todos').prop('checked', false); 1803 } 1804 if ($('input[name="voltaje[]"]:checked').not('#voltaje_todos').length === 0) { 1805 $('#voltaje_todos').prop('checked', true); 1806 } 1807 }); 1808 1809 $('#btn-aplicar-filtros').on('click', function (e) { 1810 e.preventDefault(); 1811 currentPage = 1; 1812 1813 // â FIX: Pequeño delay para asegurar que todos los eventos de cambio se procesen 1814 // Esto evita problemas de sincronización cuando se cambia rápidamente entre tipos 1815 setTimeout(function() { 1816 buscarBombas(); 1817 }, 50); 1818 }); 1819 1820 // NOTA: Evento de limpiar filtros manejado en busqueda.php 1821 // para evitar doble ejecución. Comentado aquÃ. 1822 /* 1823 $('#btn-limpiar-filtros').on('click', function (e) { 1824 e.preventDefault(); 1825 limpiarFiltros(); 1826 }); 1827 */ 1828 1829 $('#select-ordenar').on('change', function () { 1830 currentPage = 1; 1831 buscarBombas(); 1832 }); 1833 1834 $(document).on('click', '#btn-resetear-filtros', function (e) { 1835 e.preventDefault(); 1836 $('#btn-limpiar-filtros').trigger('click'); 1837 }); 1838 } 1839 1840 window.usarValoresCalculados = function (caudal, presion) { 1841 $('#input_caudal').val(caudal || currentCaudal); 1842 $('#input_presion').val(presion || currentPresion); 1843 $('.calc-modal').removeClass('active'); 1844 Modal.hide('modal-calcular-caudal'); 1845 Modal.hide('modal-calcular-presion'); 1846 $('html, body').animate({scrollTop: $('#busqueda-container').offset().top - 100}, 500); 1847 }; 1848 1849 // ======================================== 1850 // SINCRONIZACIÃN OFFCANVAS MOBILE 1851 // ======================================== 1852 1853 let filtrosClonadosFlag = false; 1854 1855 /** 1856 * Clona los filtros desktop al offcanvas mobile 1857 */ 1858 function clonarFiltrosAlOffcanvas() { 1859 if ($(window).width() <= 991 && !filtrosClonadosFlag) { 1860 console.log('ð± Clonando filtros al offcanvas mobile'); 1861 1862 const $filtrosDesktop = $('#filtros-desktop'); 1863 if (!$filtrosDesktop.length) { 1864 console.error('â No se encontró #filtros-desktop'); 1865 return; 1866 } 1867 1868 const $filtrosMobileContent = $('#filtros-mobile-content'); 1869 if (!$filtrosMobileContent.length) { 1870 console.error('â No se encontró #filtros-mobile-content'); 1871 return; 1872 } 1873 1874 // Limpiar completamente el contenedor mobile 1875 $filtrosMobileContent.empty(); 1876 1877 // Capturar valores actuales de sliders ANTES de clonar 1878 const valoresActuales = { 1879 precio_min: $('#precio_min').val(), 1880 precio_max: $('#precio_max').val(), 1881 potencia_min: $('#potencia_min').val(), 1882 potencia_max: $('#potencia_max').val() 1883 }; 1884 1885 console.log('ð Valores actuales de sliders:', valoresActuales); 1886 1887 // Clonar estructura 1888 const clone = $filtrosDesktop.clone(); 1889 clone.attr('id', 'filtros-mobile'); 1890 1891 // Cambiar IDs para evitar duplicados 1892 clone.find('[id]').each(function () { 1893 const newId = $(this).attr('id') + '_mobile'; 1894 $(this).attr('id', newId); 1895 }); 1896 1897 // Actualizar labels for 1898 clone.find('label[for]').each(function () { 1899 const forAttr = $(this).attr('for'); 1900 if (forAttr) $(this).attr('for', forAttr + '_mobile'); 1901 }); 1902 1903 // Insertar en el offcanvas 1904 $filtrosMobileContent.html(clone); 1905 1906 // Restaurar valores de sliders en los inputs hidden clonados 1907 $('#precio_min_mobile').val(valoresActuales.precio_min); 1908 $('#precio_max_mobile').val(valoresActuales.precio_max); 1909 $('#potencia_min_mobile').val(valoresActuales.potencia_min); 1910 $('#potencia_max_mobile').val(valoresActuales.potencia_max); 1911 1912 console.log('â Valores restaurados en mobile:', { 1913 precio_min: $('#precio_min_mobile').val(), 1914 precio_max: $('#precio_max_mobile').val(), 1915 potencia_min: $('#potencia_min_mobile').val(), 1916 potencia_max: $('#potencia_max_mobile').val() 1917 }); 1918 1919 filtrosClonadosFlag = true; 1920 1921 // Sincronizar valores de desktop a mobile antes de inicializar 1922 sincronizarValoresDesktopAMobile(); 1923 1924 // Inicializar sliders mobile 1925 setTimeout(function () { 1926 initSlidersRangeMobile(); 1927 console.log('â Sliders mobile inicializados'); 1928 }, 100); 1929 1930 // Inicializar eventos de marca mobile 1931 setTimeout(function () { 1932 bindMarcaCheckboxEvents(); 1933 console.log('â Eventos de marca mobile inicializados'); 1934 }, 200); 1935 1936 setTimeout(function () { 1937 if (window.__ULTIMAS_MARCAS_DISPONIBLES__) { 1938 console.log('ð± Reaplicando visibilidad marcas mobile post-clonado...'); 1939 actualizarMarcasMobileDisponibles(window.__ULTIMAS_MARCAS_DISPONIBLES__); 1940 } 1941 }, 250); 1942 } 1943 } 1944 1945 /** 1946 * Inicializa los sliders de Ion RangeSlider en mobile 1947 */ 1948 function initSlidersRangeMobile() { 1949 console.log('ðï¸ Inicializando sliders mobile'); 1950 1951 const $precioSliderMobile = $('#precio-slider_mobile'); 1952 const $potenciaSliderMobile = $('#potencia-slider_mobile'); 1953 1954 if ($precioSliderMobile.length && $precioSliderMobile.data('ionRangeSlider')) { 1955 $precioSliderMobile.data('ionRangeSlider').destroy(); 1956 } 1957 1958 if ($potenciaSliderMobile.length && $potenciaSliderMobile.data('ionRangeSlider')) { 1959 $potenciaSliderMobile.data('ionRangeSlider').destroy(); 1960 } 1961 1962 const precioMin = RANGO_PRECIO.min; 1963 const precioMax = RANGO_PRECIO.max; 1964 const potenciaMin = RANGO_POTENCIA.min; 1965 const potenciaMax = RANGO_POTENCIA.max; 1966 1967 if ($precioSliderMobile.length) { 1968 $precioSliderMobile.ionRangeSlider({ 1969 type: 'double', 1970 min: precioMin, 1971 max: precioMax, 1972 from: precioMin, 1973 to: precioMax, 1974 step: 1000, 1975 skin: 'flat', 1976 hide_min_max: true, 1977 hide_from_to: true, 1978 onChange: function (data) { 1979 $('#precio_min_mobile').val(data.from); 1980 $('#precio_max_mobile').val(data.to); 1981 $('#precio_out_min_mobile').text('$ ' + data.from.toLocaleStr
1981ing('es-CL')); 1982 $('#precio_out_max_mobile').text('$ ' + data.to.toLocaleString('es-CL')); 1983 } 1984 }); 1985 console.log('â Slider precio mobile inicializado:', precioMin, '-', precioMax); 1986 } 1987 1988 if ($potenciaSliderMobile.length) { 1989 $potenciaSliderMobile.ionRangeSlider({ 1990 type: 'double', 1991 min: potenciaMin, 1992 max: potenciaMax, 1993 from: potenciaMin, 1994 to: potenciaMax, 1995 step: 0.1, 1996 skin: 'flat', 1997 hide_min_max: true, 1998 hide_from_to: true, 1999 onChange: function (data) { 2000 $('#potencia_min_mobile').val(data.from); 2001 $('#potencia_max_mobile').val(data.to); 2002 $('#potencia_out_min_mobile').text(data.from.toFixed(1) + ' HP'); 2003 $('#potencia_out_max_mobile').text(data.to.toFixed(1) + ' HP'); 2004 } 2005 }); 2006 console.log('â Slider potencia mobile inicializado:', potenciaMin, '-', potenciaMax); 2007 } 2008 } 2009 2010 /** 2011 * Sincroniza valores de desktop a mobile (antes de abrir offcanvas) 2012 */ 2013 function sincronizarValoresDesktopAMobile() { 2014 // Sincronizar checkboxes con escapeSelector 2015 $('#filtros-desktop input[type="checkbox"]').each(function () { 2016 const idDesktop = $(this).attr('id'); 2017 const idMobile = idDesktop + '_mobile'; 2018 const isChecked = $(this).prop('checked'); 2019 2020 // Usar escapeSelector para IDs problemáticos 2021 $('#' + escapeSelector(idMobile)).prop('checked', isChecked); 2022 }); 2023 2024 // Sincronizar valores de sliders 2025 $('#precio_min_mobile').val($('#precio_min').val()); 2026 $('#precio_max_mobile').val($('#precio_max').val()); 2027 $('#potencia_min_mobile').val($('#potencia_min').val()); 2028 $('#potencia_max_mobile').val($('#potencia_max').val()); 2029 } 2030 2031 /** 2032 * Sincroniza los valores del offcanvas mobile con desktop 2033 */ 2034 function sincronizarFiltrosMobileADesktop() { 2035 console.log('ð ============================================'); 2036 console.log('ð SINCRONIZANDO FILTROS MOBILE â DESKTOP'); 2037 console.log('ð ============================================'); 2038 2039 // â 1. Sincronizar checkboxes de voltaje 2040 console.log('1ï¸â£ Sincronizando voltajes...'); 2041 $('#filtros-mobile input[name="voltaje[]"]').each(function () { 2042 const idMobile = $(this).attr('id'); 2043 const idDesktop = idMobile.replace('_mobile', ''); 2044 const isChecked = $(this).prop('checked'); 2045 2046 // Usar escapeSelector para IDs problemáticos 2047 $('#' + escapeSelector(idDesktop)).prop('checked', isChecked); 2048 2049 if (isChecked) { 2050 console.log(' â Voltaje:', idDesktop); 2051 } 2052 }); 2053 2054 // â 2. Sincronizar checkboxes de tipo 2055 console.log('2ï¸â£ Sincronizando tipos de bomba...'); 2056 $('#filtros-mobile input[name="tipo_bomba[]"]').each(function () { 2057 const idMobile = $(this).attr('id'); 2058 const idDesktop = idMobile.replace('_mobile', ''); 2059 const isChecked = $(this).prop('checked'); 2060 2061 // Usar escapeSelector para IDs problemáticos 2062 $('#' + escapeSelector(idDesktop)).prop('checked', isChecked); 2063 2064 if (isChecked) { 2065 console.log(' â Tipo:', idDesktop); 2066 } 2067 }); 2068 2069 // â 3. Sincronizar checkboxes de marca mobile â Select2 desktop 2070 console.log('3ï¸â£ Sincronizando marcas (checkboxes â Select2)...'); 2071 const marcasMobile = $('#filtros-mobile input[name="marca_checkbox[]"]:checked') 2072 .not('#marca_todas_mobile') 2073 .map(function () { 2074 return $(this).val(); 2075 }) 2076 .get(); 2077 2078 console.log(' Marcas seleccionadas en mobile:', marcasMobile.length > 0 ? marcasMobile : 'ninguna'); 2079 2080 const $marcaDesktop = $('#select-marca'); 2081 if ($marcaDesktop.length) { 2082 $marcaDesktop.val(marcasMobile).trigger('change'); 2083 } 2084 2085 // â 4. Sincronizar valores de sliders 2086 console.log('4ï¸â£ Sincronizando sliders...'); 2087 const precioMinMobile = $('#precio_min_mobile').val(); 2088 const precioMaxMobile = $('#precio_max_mobile').val(); 2089 const potenciaMinMobile = $('#potencia_min_mobile').val(); 2090 const potenciaMaxMobile = $('#potencia_max_mobile').val(); 2091 2092 console.log(' Precio mobile:', precioMinMobile, '-', precioMaxMobile); 2093 console.log(' Potencia mobile:', potenciaMinMobile, '-', potenciaMaxMobile); 2094 2095 $('#precio_min').val(precioMinMobile); 2096 $('#precio_max').val(precioMaxMobile); 2097 $('#potencia_min').val(potenciaMinMobile); 2098 $('#potencia_max').val(potenciaMaxMobile); 2099 2100 console.log(' Precio desktop actualizado:', $('#precio_min').val(), '-', $('#precio_max').val()); 2101 console.log(' Potencia desktop actualizado:', $('#potencia_min').val(), '-', $('#potencia_max').val()); 2102 2103 // â 5. Actualizar sliders desktop visualmente 2104 const $precioSliderDesktop = $('#precio-slider'); 2105 const $potenciaSliderDesktop = $('#potencia-slider'); 2106 2107 if ($precioSliderDesktop.length && $precioSliderDesktop.data('ionRangeSlider')) { 2108 const precioMin = parseFloat(precioMinMobile) || RANGO_PRECIO.min; 2109 const precioMax = parseFloat(precioMaxMobile) || RANGO_PRECIO.max; 2110 2111 $precioSliderDesktop.data('ionRangeSlider').update({ 2112 from: precioMin, 2113 to: precioMax 2114 }); 2115 console.log(' â Slider precio desktop actualizado visualmente'); 2116 } 2117 2118 if ($potenciaSliderDesktop.length && $potenciaSliderDesktop.data('ionRangeSlider')) { 2119 const potenciaMin = parseFloat(potenciaMinMobile) || RANGO_POTENCIA.min; 2120 const potenciaMax = parseFloat(potenciaMaxMobile) || RANGO_POTENCIA.max; 2121 2122 $potenciaSliderDesktop.data('ionRangeSlider').update({ 2123 from: potenciaMin, 2124 to: potenciaMax 2125 }); 2126 console.log(' â Slider potencia desktop actualizado visualmente'); 2127 } 2128 2129 console.log('â SINCRONIZACIÃN COMPLETADA'); 2130 console.log('ð ============================================\n'); 2131 } 2132 2133 /** 2134 * Abre el offcanvas de filtros 2135 */ 2136 function abrirOffcanvasFiltros() { 2137 clonarFiltrosAlOffcanvas(); 2138 $('#filtros-offcanvas').addClass('active'); 2139 $('#filtros-overlay').addClass('active'); 2140 $('body').css('overflow', 'hidden'); 2141 } 2142 2143 /** 2144 * Cierra el offcanvas de filtros 2145 */ 2146 function cerrarOffcanvasFiltros() { 2147 $('#filtros-offcanvas').removeClass('active'); 2148 $('#filtros-overlay').removeClass('active'); 2149 $('body').css('overflow', 'auto'); 2150 } 2151 2152 /** 2153 * Limpia completamente el offcanvas mobile 2154 */ 2155 function limpiarOffcanvasMobile() { 2156 $('#filtros-mobile-content').empty(); 2157 filtrosClonadosFlag = false;
2158 console.log('ðï¸ Offcanvas mobile limpiado'); 2159 } 2160 2161 // Eventos del offcanvas 2162 // NOTA: Los eventos del offcanvas ahora están manejados en busqueda.php 2163 // para evitar duplicación de sliders. Se mantienen solo como fallback. 2164 // $('#btn-abrir-filtros').on('click', abrirOffcanvasFiltros); 2165 $('#btn-cerrar-filtros').on('click', cerrarOffcanvasFiltros); 2166 $('#filtros-overlay').on('click', cerrarOffcanvasFiltros); 2167 2168 $(document).on('keydown', function (e) { 2169 if (e.key === 'Escape' && $('#filtros-offcanvas').hasClass('active')) { 2170 cerrarOffcanvasFiltros(); 2171 } 2172 }); 2173 2174 // NOTA: Eventos de aplicar/limpiar filtros mobile están manejados en busqueda.php 2175 // para evitar doble ejecución de búsquedas. Se comentan aquÃ. 2176 /* 2177 $('#btn-aplicar-filtros-mobile').on('click', function () { 2178 console.log('ð± Aplicando filtros desde mobile...'); 2179 sincronizarFiltrosMobileADesktop(); 2180 cerrarOffcanvasFiltros(); 2181 setTimeout(function () { 2182 console.log('ð Ejecutando búsqueda con filtros mobile'); 2183 currentPage = 1; 2184 buscarBombas(); 2185 }, 200); 2186 }); 2187 */ 2188 2189 /* 2190 $('#btn-limpiar-filtros-mobile').on('click', function () { 2191 console.log("ð§½ Limpiando filtros MOBILE..."); 2192 $('#filtros-mobile #voltaje_todos_mobile').prop('checked', true); 2193 $('#filtros-mobile input[name="voltaje[]"]').not('#voltaje_todos_mobile').prop('checked', false); 2194 $('#filtros-mobile #tipo_todos_mobile').prop('checked', true); 2195 $('#filtros-mobile input[name="tipo_bomba[]"]').not('#tipo_todos_mobile').prop('checked', false); 2196 $('#filtros-mobile #marca_todas_mobile').prop('checked', true); 2197 $('#filtros-mobile input[name="marca_checkbox[]"]').not('#marca_todas_mobile').prop('checked', false); 2198 const $precioSliderMobile = $('#precio-slider_mobile'); 2199 const $potenciaSliderMobile = $('#potencia-slider_mobile'); 2200 if ($precioSliderMobile.length && $precioSliderMobile.data('ionRangeSlider')) { 2201 $precioSliderMobile.data('ionRangeSlider').update({ 2202 from: RANGO_PRECIO.min, 2203 to: RANGO_PRECIO.max 2204 }); 2205 } 2206 if ($potenciaSliderMobile.length && $potenciaSliderMobile.data('ionRangeSlider')) { 2207 $potenciaSliderMobile.data('ionRangeSlider').update({ 2208 from: RANGO_POTENCIA.min, 2209 to: RANGO_POTENCIA.max 2210 }); 2211 } 2212 $('#precio_min_mobile, #precio_max_mobile, #potencia_min_mobile, #potencia_max_mobile').val(''); 2213 sincronizarFiltrosMobileADesktop(); 2214 cerrarOffcanvasFiltros(); 2215 setTimeout(function () { 2216 currentPage = 1; 2217 buscarBombas(); 2218 }, 200); 2219 console.log("â Filtros mobile limpiados correctamente."); 2220 }); 2221 */ 2222 2223// Resetear cuando cambia el tamaño de ventana 2224 $(window).on('resize', function () { 2225 if ($(window).width() > 991) { 2226 cerrarOffcanvasFiltros(); 2227 limpiarOffcanvasMobile(); 2228 } 2229 }); 2230 2231// Monitor para re-inicializar Select2 si se pierde 2232 setInterval(function () { 2233 if ($(window).width() > 991) { // solo desktop 2234 const $sel = $('#select-marca'); 2235 if ($sel.length && !$sel.hasClass('select2-hidden-accessible')) { 2236 initSelect2Marca(); 2237 } 2238 } 2239 }, 2000); 2240})(jQuery); 2241 2242 2243// Bootstrap tooltips 2244document.addEventListener('DOMContentLoaded', function () { 2245 var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); 2246 var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { 2247 return new bootstrap.Tooltip(tooltipTriggerEl); 2248 }); 2249}); 2250 2251// Función global de cotización 2252function cotizarProductoCalculadora(elemento) { 2253 let productoId = ''; 2254 let productoNombre = ''; 2255 let productoImagen = '';
2256 let cantidadSeleccionada = '1'; 2257 2258 if (elemento && elemento.tagName === 'BUTTON') { 2259 productoId = elemento.getAttribute('data-producto-id') || ''; 2260 productoNombre = elemento.getAttribute('data-producto-nombre') || ''; 2261 productoImagen = elemento.getAttribute('data-producto-imagen') || ''; 2262 2263 if (!productoId || !productoNombre || !productoImagen) { 2264 const card = elemento.closest('.koslan-bomba-card'); 2265 if (card) { 2266 if (!productoId) productoId = card.getAttribute('data-producto-id') || ''; 2267 if (!productoNombre) { 2268 const t = card.querySelector('.koslan-bomba-titulo'); 2269 productoNombre = t ? t.textContent.trim() : ''; 2270 } 2271 if (!productoImagen) { 2272 const img = card.querySelector('.koslan-bomba-imagen img'); 2273 productoImagen = img ? img.src : ''; 2274 } 2275 } 2276 } 2277 } else if (elemento && typeof elemento === 'object' && !elemento.tagName) { 2278 productoId = elemento.id || elemento.productoId || ''; 2279 productoNombre = elemento.nombre || elemento.productoNombre || ''; 2280 productoImagen = elemento.imagen || elemento.productoImagen || '';
2281 cantidadSeleccionada = elemento.cantidad || '1'; 2282 } 2283 2284 if (!productoId || !productoNombre) { 2285 console.error('Faltan datos del producto'); 2286 alert('Error: No se pudieron obtener los datos del producto.'); 2287 return; 2288 } 2289 if (!productoImagen) productoImagen = 'https://koslan.kinsta.cloud/wp-content/uploads/2025/10/IMG_DEFAULT.webp'; 2290 2291 if (typeof abrirSidebarCotizacion === 'function') { 2292 abrirSidebarCotizacion(productoId, productoNombre, productoImagen, cantidadSeleccionada); 2293 } else { 2294 console.error('La función abrirSidebarCotizacion no está disponible'); 2295 alert('Error: Sistema de cotización no disponible.'); 2296 } 2297 2298} 2299 2300document.addEventListener('DOMContentLoaded', function () { 2301 document.body.addEventListener('click', function (e) { 2302 const boton = e.target.closest('.koslan-btn-cotizar'); 2303 if (boton) { 2304 e.preventDefault(); 2305 cotizarProductoCalculadora(boton); 2306 } 2307 }); 2308});
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.