PageSourceSearch

https://www.koslan.cl/wp-content/plugins/calculadora-koslan-v2_5_9-FIXED-FINAL/assets/js/script.js?ver=2.5.8

js koslan.cl collected 2026-10-02 15:40:42 UTC 94,223 bytes, 2,308 lines download raw bytes

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.