PageSourceSearch

https://www.farmacianeusmartinez.cat/assets/common/js/newCart.js?v=2026092902

js farmacianeusmartinez.cat collected 2026-10-04 22:15:01 UTC 55,088 bytes, 1,359 lines download raw bytes

1const classUpdateShippingAmountCheckbox = '.shipping-amount-price';
2const classUpdateTotalBeforeDiscount = '.layette-total-before-discount-shipping';
3const discountBoxSummary = '#summary-discount-box';
4const totalBeforeDiscountLoggedFP= '#total-before-discount-shipping-logged';
5const noDiscountTotalsSummary = '.no-discount-totals-summary';
6const discountTotalsSummary = '.discount-totals-summary';
7const summaryAccumulatedPoints = '#summary-accumulated-points';
8const summaryRedeemedPoints = '#summary-redeemed-points';
9const summaryTotalPoints = '#summary-total-points';
10const summaryRemainingPoints = '#summary-remaining-points'
11const productShowInputPointsSelected = '#points-payment-method'
12const productShowRemainingUserPoints = '#user-points-value'
13
14/**
15 * Clau d'una linia de carret per als ids de DOM. Amb variants, un mateix producte ocupa
16 * diverses files i l'id de contingut sol quedava duplicat. Sense variant, clau de sempre.
17 */
18function foLineKey(productId, axisValueId) {
19    return axisValueId ? productId + '-v' + axisValueId : '' + productId;
20}
21
22var deleteLayetteItem = function (buttonSelector, callback) {
23    var href = buttonSelector.first().attr('data-href');
24    var productId = buttonSelector.first().attr('data-productid');
25    $.ajax({
26        url: href, method: 'post', beforeSend: function () {
27            buttonSelector.toggleClass('hide');
28        }, success: function (data) {
29            if (callback != "undefined") {
30                callback(data);
31            }
32
33            //show/hide trans
34            if(data['items_count'] > 1 || data['items_count'] === 0){
35                $('.articles-count-plural').show();
36                $('.articles-count-singular').hide();
37            }else{
38                $('.articles-count-singular').show();
39                $('.articles-count-plural').hide();
40            }
41
42            //new cart update values
43            $('.count-product-carts').html(data['items_count']);
44            $(classUpdateShippingAmountCheckbox).html(formatCurrency(data.shipping_amount)).fadeIn();
45            $('.shipping-amount-delivery').html(formatCurrency(data.shipping_amount_delivery)).fadeIn();
46            $('.layette-total-net').html(formatCurrency(data.subtotal)).fadeIn();
47            $(classUpdateTotalBeforeDiscount).html(formatCurrency(data.totalBeforeDiscount)).fadeIn();
48            $('.layette-total-net-shipping').html(formatCurrency(data.total)).fadeIn();
49
50            //APPLY DISCOUNT SUMMARY LINES
51            getDiscountLines(data)
52
53            // Update max amount warning after removing product
54            $('#cart-max-amount-alert').remove();
55            if (data.exceeds_max_amount && data.max_amount_error_message) {
56                $('.cart-summary .btn-layette-content').first().before(
57                    '<div class="alert alert-danger" id="cart-max-amount-alert" style="margin-bottom:10px;">' +
58                    '<i class="fa fa-exclamation-triangle"></i> ' + data.max_amount_error_message +
59                    '</div>'
60                );
61                $('.payment-cart-button').prop('disabled', true).addClass('disabled');
62            } else {
63                $('.payment-cart-button').prop('disabled', false).removeClass('disabled');
64            }
65
66            // FARMAPREMIUM POINTS TOTAL (ORISSENS)
67            if (data.points) {
68                $(summaryAccumulatedPoints).html('+'+formatPoints(data.points.accumulated_points));
69                $(summaryRedeemedPoints).html('-'+formatPoints(data.points.redeemed_points));
70                $(summaryTotalPoints).html(formatPoints(data.points.total_points));
71                $(summaryRemainingPoints).html(formatPoints(data.points.remaining_points));
72            }
73
74            $('.cart-count').html('(' + data['items_count'] + ')');
75
76            //left free shipping (minShipping - subtotal) TODO
77            getLeftFreeShipping(data['freeShippingLeft']);
78
79            // Toggle home delivery / no transport based on allowedHomeDelivery
80            if (data.allowedHomeDelivery === false || data.allowedHomeDelivery === null) {
81                $('label[for="home_delivery"]').hide();
82                $('#home_delivery').prop('checked', false);
83                $('#no_transport').prop('checked', true);
84                $('.shipping-transport').hide();
85                // Hide address fields and add notShippable hidden input
86                $('#address, #postal_code, #town_id').closest('.col-md-12, .col-md-6').hide();
87                if ($('input[name="notShippable"]').length === 0) {
88                    $('#address').closest('.row').append('<input name="notShippable" value="1" type="hidden"/>');
89                }
90            } else if (data.allowedHomeDelivery === true) {
91                $('label[for="home_delivery"]').show();
92                $('#home_delivery').prop('checked', true);
93                $('#no_transport').prop('checked', false);
94                $('.shipping-transport').show();
95                // Show address fields and remove notShippable hidden input
96                $('#address, #postal_code, #town_id').closest('.col-md-12, .col-md-6').show();
97                $('input[name="notShippable"]').remove();
98            }
99
100            if($('.circle-count').length > 0){
101                if(data['count'] > 0){
102                    $('.circle-count').show();
103                }else{
104                    $('.circle-count').hide();
105                }
106            }
107
108            //dont allow next step
109            if(data['items_count'] === 0){
110                $('.cart-button-next').removeAttr("href").addClass('disabled');
111                $('#payment_button').removeAttr("href").addClass('disabled');
112                $('#payment_button').prop("disabled", true);
113                $('.payment-cart-button').removeAttr("href").addClass('disabled');
114                $('.payment-cart-button').prop("disabled", true);
115                $('.no-articles').show();
116                $('.count-show').hide();
117                $('.potential-discount-container').hide();
118            }
119
120            if (data == 0) {
121                $('#layette-custom-actions').fadeOut();
122                $('.stickyCartButton').fadeOut();
123            }
124        }
125    });
126};
127
128function formatPoints(points) {
129    if (typeof points === 'number') {
130        return points.toLocaleString('es-ES') + ' pts';
131    }
132}
133
134function setErrorOnStockChange() {
135    let element = $('#products_no_stock');
136
137    if (element.length) {
138        let ids = element.data('ids');
139        let error_text = element.data('error');
140
141        if (typeof ids === "number") {
142            ids = [ids];
143        }
144
145        ids.forEach(function(id) {
146            $('#cart-error-' + id)
147                .show()
148                .removeClass('hide')
149                .find('span')
150                .html(error_text);
151        });
152    }
153}
154
155
156/*
157 DOCUMENT LOADED
158 */
159
160$(document).ready(function () {
161
162    // Quantity changer
163    jQuery('<div class="quantity-nav"><button class="quantity-button quantity-up"><i class="fa fa-chevron-up"></i></button><button class="quantity-button quantity-down"><i class="fa fa-chevron-down"></i></button></div>').insertAfter('.quantity input');
164    $('[data-toggle="tooltip"]').tooltip();
165
166    // TODO QTT
167    $(document).on("click", ".product-update-qty", function() {
168        var quantity = $(".product-quantity-block input");
169        var min_quantity = 0;
170        var max_quantity = null;
171        var limit_stock = false;
172        var current_quantity = parseInt(quantity.val());
173
174        if (typeof quantity.attr("min") != "undefined") {
175            min_quantity = quantity.attr("min");
176        }
177
178        if (typeof quantity.attr("max") != "undefined") {
179            max_quantity = quantity.attr("max");
180        }
181
182        if (typeof quantity.attr("limit-stock") != "undefined") {
183            limit_stock = quantity.attr("limit-stock");
184        }
185
186        if (isNaN(quantity.val())) {
187            quantity.val(0);
188        }
189
190        if ($(this).attr("data-mode") == "up") {
191            if (!limit_stock || current_quantity < max_quantity) {
192                quantity.val(current_quantity+1);
193            }
194        } else {
195            if (parseInt(quantity.val()) > min_quantity) {
196                quantity.val(current_quantity-1);
197            }
198        }
199    });
200
201    // TODO ADD PRODUCT TO CART
202
203    $('body').on('click', 'a.layette-add-product', function (event) {
204        event.preventDefault();
205        // No afegir si el boto esta deshabilitat (p.ex. producte amb variants sense variant
206        // seleccionada). El boto es un <a>, aixi que prop('disabled') no te efecte; cal la classe.
207        if ($(this).hasClass('disabled')) {
208            return;
209        }
210        // if we add products from searcher engine in cart, reload (like dosfarma)
211        var reloadAfterAdd = false;
212        if ($(this).attr('data-check-reload')) {
213            var currentLocation = $(location).attr('href');
214            if ( currentLocation.includes('cistella') || currentLocation.includes('cesta')
215                || currentLocation.includes('cesta') || currentLocation.includes('cart')
216                || currentLocation.includes('panier') ) {
217                reloadAfterAdd = true;
218            }
219        }
220
221        var product_id = $(this).attr('data-id');
222        var paymentRadio = $('input[name="payment_option"]:checked');
223        var payment_method = paymentRadio.length ? paymentRadio.val() : null;
224        var $addButtons = $('.ecommerce-product-add-button-' + product_id);
225        var custom = $(this).attr('data-custom');
226        var quantitySelector = $('input[name=quantity_' + product_id + ']');
227
228        if (quantitySelector.length != 0) {
229            var quantity = quantitySelector.val();
230        } else {
231            var quantity = 1;
232        }
233
234        var enableQuantity = $(this).hasClass("quantity-always-enabled");
235        $('#cart-error').hide();
236
237        $('#cart-error-general').addClass("hidden");
238
239        //Check if this content need reserve date
240        var reserve = processReservationDate($(this), $('input[name=type_guest]'));
241        var axisValueId = $(this).attr('data-axis-value-id');
242        var postData = {
243            'reserve': reserve,
244            'payment_method': payment_method
245        };
246        if (axisValueId) {
247            postData['axis_value_id'] = axisValueId;
248        }
249        $.ajax({
250            url: $(this).attr('data-href') + '?product_id=' + product_id + '&quantity=' + quantity + '&custom=' + custom,
251            beforeSend: function () {
252                //button.toggleClass('hide');
253                $addButtons.toggleClass('hide');
254            },
255            data: postData,
256            method: 'post',
257            success: function (data) {
258
259                if (typeof data.error !== 'undefined') {
260                    $addButtons.toggleClass('hide');
261                    $('#cart-error-general').removeClass('hidden');
262                    $('#cart-error-general').html(data.error);
263                    return;
264                }
265
266                updateCartModal(data);
267
268                var quantity = $('.product-quantity-input');
269                var quantitySelector = $('input[name=quantity_' + data.product_id + ']');
270                var $removeButtons = $('.ecommerce-product-remove-button-' + data.product_id);
271
272                renderProductPoints(data, null, product_id, payment_method);
273
274                quantity.val(data.product_quantity);
275                quantity.attr('data-productid', data.product_id);
276                quantity.attr('data-quantity', data.product_quantity);
277                // Sense aixo, canviar la quantitat des del modal enviava la peticio sense eix.
278                if (data.axis_value_id) {
279                    quantity.attr('data-axis-value-id', data.axis_value_id);
280                } else {
281                    quantity.removeAttr('data-axis-value-id');
282                }
283
284                updateQuantityInput(data, payment_method);
285
286                $('.quantity-nav').remove();
287                jQuery('<div class="quantity-nav"><button class="quantity-button quantity-up"><i class="fa fa-chevron-up"></i></button><button class="quantity-button quantity-down"><i class="fa fa-chevron-down"></i></button></div>').insertAfter(quantity);
288
289                if (!reloadAfterAdd) {
290                    $('#cartModal').modal('show');
291                }
292
293                $removeButtons.toggleClass('hide');
294                if (typeof enableQuantity !== "undefined" && !en
294ableQuantity) {
295                    quantitySelector.attr('disabled', '1');
296                }
297                $('#layette-custom-actions').fadeIn();
298
299                var $stickyCartButton = $('.stickyCartButton');
300                var $stickyCartCount = $('#sticky-Cart-Count');
301                $stickyCartCount.html(data.cartInfo);
302                if ($stickyCartCount.html() != 0) {
303                    $stickyCartButton.fadeIn();
304                } else {
305                    $stickyCartButton.fadeOut();
306                }
307
308                if ($('.circle-count').length > 0) {
309                    if (data.count > 0) {
310                        $('.circle-count').show();
311                    } else {
312                        $('.circle-count').hide();
313                    }
314                }
315
316                // Google Tag Manager/eventos de analytics
317                window.dataLayer = window.dataLayer || [];
318                function gtag() { dataLayer.push(arguments); }
319                dataLayer.push({ ecommerce: null });  // Limpiar objeto anterior
320                gtag("event", data.event_type, {
321                    currency: "EUR",
322                    value: data.total,
323                    items: [{
324                        item_id: data.product_id,
325                        item_name: data.product_name,
326                        price: data.payment_method === 'points' ? data.points_spent : data.product_price_quantity_after_offer,
327                        quantity: data.product_quantity
328                    }]
329                });
330
331                if (reloadAfterAdd) {
332                    location.reload();
333                }
334            },
335            error: function (thrownError) {
336                $addButtons.toggleClass('hide');
337                $('#cart-error-general').removeClass("hidden");
338                $('#cart-error-general').html(thrownError.statusText || thrownError.text);
339            }
340        });
341    });
342
343    function processReservationDate($this, $isGuest = null){
344        if ($isGuest.val() === '1'){
345            source = $('input[name=source]').val();
346            appointment_at_set = $('input[name=appointment_at_set]').val();
347            type = $('input[name=type]').val();
348            duration = $('input[name=duration]').val();
349            content_id = $this.attr('data-id')
350            dates = $(".stepperBar-step1 span").text() + ', ' + $(".stepperBar-step2 span").text();
351            return {'source': source, 'appointment_at_set': appointment_at_set, 'type': type, 'content_id': content_id, 'duration': duration, 'nvgues': 1, 'price': '22', 'reserve': '1', 'dates': dates};
352        } else {
353            return null;
354        }
355    }
356
357    $('body').on('click', '.btn-layette-disabled', function () {
358        var button = $(this);
359        var addProductButton = $('#layette-add-product-' + button.attr('data-id'));
360        var quantitySelector = $('select[name=quantity_' + button.attr('data-id') + ']');
361        deleteLayetteItem($(this), function (data) {
362            addProductButton.toggleClass('hide');
363            quantitySelector.removeAttr('disabled');
364        });
365    });
366
367    $('body').on('click', '.layette-remove-line-checkout', function () {
368        var that = $(this);
369        var removedLineKey = that.attr('data-line-key')
370            || foLineKey(that.attr('data-productid'), that.attr('data-axis-value-id'));
371        deleteLayetteItem($(this), function (data) {
372            $('#layette-product-' + removedLineKey).fadeOut(500, function () {
373                that.toggleClass('hide');
374            });
375
376            if (
377                data.total == 0.00 ||
378                !data.lines || !Array.isArray(data.lines) || data.lines.length === 0
379            ) {
380                const targetUrl = $('#customLayetteIndexLink').attr('href');
381
382                if (targetUrl) {
383                    document.location.href = targetUrl;
384                }
385            }
386
387            window.dataLayer = window.dataLayer || [];
388            function gtag(){dataLayer.push(arguments);}
389            dataLayer.push({ ecommerce: null });  // Clear the previous ecommerce object.
390
391            gtag("event", data['event_type'], {
392                currency: "EUR",
393                value: data['total'],
394                items: [{
395                    item_id: data['product_id'],
396                    item_name: data['product_name'],
397                    price: data['product_price_quantity_after_offer'],
398                    quantity: data['product_quantity']
399                }]
400            })
401
402        });
403    });
404
405    $('body').on('click', '#no_transport', function () {
406        const $this = $(this);
407        const noTransport = $this.prop('checked');
408
409        // Limpiar errores anteriores
410        $this.find('.fop-error-container').each(function () {
411            $(this).removeClass('fop-error-container');
412            $(this).find('.fop-error-right').remove();
413        });
414
415        sendDeliveryOptionRequest($this, {
416            no_transport: noTransport
417        });
418    });
419
420    $('body').on('click', '#home_delivery', function () {
421        const $this = $(this);
422        let noTransport = $('#no_transport').prop('checked');
423        let homeDelivery = $('#home_delivery').prop('checked');
424        let clickAndCollect = $('#click_and_collect').prop('checked');
425
426        if (homeDelivery) {
427            noTransport = false;
428            clickAndCollect = false;
429        }
430
431        $('#home_delivery').prop("checked", !noTransport);
432        $('#home_delivery').prop("disabled", !noTransport);
433
434        sendDeliveryOptionRequest($this, {
435            no_transport: noTransport,
436            click_and_collect: clickAndCollect
437        });
438    });
439
440    $('body').on('click', '#click_and_collect', function () {
441        const $this = $(this);
442        const clickAndCollect = $this.prop('checked');
443
444        // Limpiar errores anteriores
445        $this.find('.fop-error-container').each(function () {
446            $(this).removeClass('fop-error-container');
447            $(this).find('.fop-error-right').remove();
448        });
449
450        if (clickAndCollect) {
451            $('#no_transport').prop("checked", true).prop("disabled", true).css("cursor", "not-allowed");
452        } else {
453            $('#no_transport').prop("checked", false).prop("disabled", false).css("cursor", "pointer");
454        }
455
456        sendDeliveryOptionRequest($this, {
457            no_transport: $('#no_transport').prop('checked'),
458            click_and_collect: clickAndCollect
459        });
460    });
461
462    //new buttons quantity
463    $('body').on('click', '.quantity-up', function (e) {
464        e.preventDefault();
465        const $input = $(this).closest('.quantity').find('input.product-quantity-input');
466        if ($input.length === 0) return;
467
468        const max = parseInt($input.attr('max')) || Infinity;
469        const currentVal = parseInt($input.val()) || 1;
470
471        if (!inputChangeCanRedeemMorePoints($input)) return false;
472
473        const newVal = Math.min(currentVal + 1, max);
474        updateQuantity($input, newVal);
475    });
476
477    $('body').on('click', '.quantity-down', function (e) {
478        e.preventDefault();
479        const $input = $(this).closest('.quantity').find('input.product-quantity-input');
480        const min = parseInt($input.attr('min')) || 1;
481        const currentVal = parseInt($input.val()) || 1;
482
483        const newVal = Math.max(currentVal - 1, min);
484        if (newVal !== currentVal) {
485            updateQuantity($input, newVal);
486        }
487    });
488
489    $('body').on('change', 'select.layette_quantity_input, select.layette_quantity_input_carrousel',  function () {
490        var $select = $(this);
491        var selectName = $select.attr('name');
492        $('select.layette_quantity_input[name=' + selectName + ']').not(this).val($select.val());
493        $('select.layette_quantity_input_carrousel[name=' + selectName + ']').not(this).val($select.val());
494    });
495
496    //not used?
497    $('body').on('click', '.remodal-link', function (event) {
498        event.preventDefault();
499        $.ajax({
500            url: $(this).attr('href'), beforeSend: function () {
501                $('#remodal').remove();
502                $('<div id="remodal"><div class="sk-cube-grid"><div class="sk-cube1 sk-cube"></div><div class="sk-cube2 sk-cube"></div><div class="sk-cube3 sk-cube"></div><div class="sk-cube4 sk-cube"></div><div class="sk-cube5 sk-cube"></div><div class="sk-cube6 sk-cube"></div><div class="sk-cube7 sk-cube"></div><div class="sk-cube8 sk-cube"></div><div class="sk-cube9 sk-cube"></div></div></div>').appendTo('body').remodal().open();
503            }, success: function (data) {
504                $('#remodal').html(data);
505            }
506        });
507    });
508
509    //promo codes
510    $('body').on('click', '#validate_code_cart',  function (event) {
511
512        event.preventDefault();
513        var $this = $(this);
514        var validate_code_url = $this.attr("data-urlvalidate");
515        var promo_code = $('#promo_code').val();
516        var current_step = $this.attr('data-step');
517
518        // remove fop-error message
519        $(this).parent().each(function () {
520            $(this).removeClass('fop-error-container')
521            $(this).find('.fop-error-right').remove()
522            $('#validate_code_cart').css("top", "10px")
523        })
524        var $button = $(this).find('[type=submit]').last();
525        $.ajax({
526            type: "POST",
527            url: validate_code_url,
528            cache: false,
529            data: {'promo_code': promo_code, 'current_step': current_step},
530            dataType: 'json',
531            success: function (data) {
532                if (data.success) {
533                    $('#validate_code_cart').css("top", "10px");
534
535                    //call to function that renders section from html render on laravel controller
536                    setRenderCartSectionsFromController(data)
537
538                    $('#promoCodeOk').show();
539                    $('#promoCodeOk').removeClass('hide');
540
541                    $('#promoCodeError').hide();
542                    $('#promoCodeError').addClass('hide');
543
544                    $('#box-promo-code').hide();
545                    $('#box-promo-code').addClass('hide');
546
547                } else {
548                    if(data.status === 1){
549                        $('#promoCodeError').hide();
550                        $('#promoCodeError').addClass('hide');
551                        $('.fop-error-right').remove();
552                        for (var i = 0, len = data.errors.length; i < len; i++) {
553                            $('[name=' + data.errors[i].field + ']').parent().before("<span class='fop-error-right'>" + data.errors[i].message + "</span>");
554                            $('[name=' + data.errors[i].field + ']').parent().parent().addClass('fop-error-container');
555                        }
556                        $('#validate_code_cart').css("top", "35px");
557                    }else{
558                        $('#validate_code_cart').css("top", "10px");
559                        $('#promoCodeError').show();
560                        $('#promoCodeError').removeClass('hide');
561                        $('#promoCodeOk').hide();
562                        $('#promoCodeOk').addClass('hide');
563
564                        if(data.error == 'codeko') {
565                            $('#messageCodeKo').show();
566                            $('#messageCodeKo').removeClass('hide');
567                        } else if(data.error == 'codeko_applyold'){
568                            $('#messageCodeOld').show();
569                            $('#messageCodeOld').removeClass('hide');
570                        }
571                    }
572
573
574                }
575            },
576            complete: function() {
577                $button.show();
578            }
579        });
580    });
581
582    $('body').on('click', '#delete_code_cart',  function (event) {
583        event.preventDefault();
584        var $this = $(this);
585        var validate_code_url = $this.attr('data-urldelete');
586
587        $this.find('.fop-error-container').each(function () {
588            $this.removeClass('fop-error-container');
589            $this.find('.fop-error-right').remove();
590        });
591        var $button = $this.find('[type=submit]').last();
592        $.ajax({
593            type: "POST",
594            url: validate_code_url,
595            cache: false,
596            dataType: 'json',
597            success: function (data) {
598                if (data.success) {
599                    //call to function that renders section from html render on laravel controller
600                    setRenderCartSectionsFromController(data)
601                }
602            },
603            complete: function() {
604                $button.show();
605            }
606        });
607    });
608
609    // dropdown FP LOGIN
610    $(".header-farmapremium-login").on("click", function (event) {
611        event.stopPropagation();
612        let $dropdown = $(".dropdown-menu-fp-login");
613
614        if ($dropdown.is(":visible")) {
615            $dropdown.hide();
616        } else {
617            $(".dropdown-menu-fp-login").hide();
618            $dropdown.css("display", "block");
619        }
620    });
621
622    $(document).on("click", function (event) {
623        if (!$(event.target).closest(".farmapremium-dropdown").length) {
624            $(".dropdown-menu-fp-login").hide();
625        }
626    });
627
628    // TODO revisar per multiples templates
629    $('#show-mobile-farmapremium').click(function (event) {
630        event.preventDefault();
631        $('#mobile-farmapremium').toggle();
632        const currentOverflow = $("body").css("overflow");
633        $("body").css("overflow", currentOverflow === "hidden" ? "scroll" : "hidden");
634    });
635
636    $('#close-mobile-menu-fp').click(function (event) {
637        event.preventDefault();
638        $('#mobile-farmapremium').hide();
639        const currentOverflow = $("body").css("overflow");
640        $("body").css("overflow", currentOverflow === "hidden" ? "scroll" : "hidden");
641    })
642
643
644    setErrorOnStockChange();
645    fireOnchangeProductQuantity();
646});
647
648
649function getLeftFreeShipping(freeShippingLeft) {
650
651    if(freeShippingLeft !== 0){
652        $('#resume-shipping-left').show();
653        $('#shipping-left').html(freeShippingLeft+ '€');
654    }else{
655        $('#resume-shipping-left').hide();
656    }
657
658    // Update modal free shipping message
659    var $modalShipping = $('#modal-free-shipping-left');
660    if ($modalShipping.length) {
661        var textLeft = $modalShipping.attr('data-text-left');
662        var textReached = $modalShipping.attr('data-text-reached');
663        var shippingLeft = freeShippingLeft;
664
665        // If backend returns null, calculate from min and subtotal
666        if (shippingLeft === null || typeof shippingLeft === 'undefined') {
667            var min = parseFloat($modalShipping.attr('data-freeshipping-min'));
668            var subtotalText = $('#total-price-cart').text().replace('€', '').trim().replace(',', '.');
669            var subtotal = parseFloat(subtotalText) || 0;
670            if (min > 0 && subtotal > 0) {
671                var left = min - subtotal;
672                shippingLeft = left > 0 ? left.toFixed(2).replace('.', ',') : 0;
673            }
674        }
675
676        if (shippingLeft && shippingLeft !== 0 && shippingLeft !== '0') {
677            $modalShipping.find('.free_shipping-modal__text').html(textLeft.replace(':amount', shippingLeft));
678        } else {
679            $modalShipping.find('.free_shipping-modal__text').html(textReached);
680        }
681        $modalShipping.css('display', 'flex');
682    }
683
684}
685
686// "Read more" funcionality for aside elements
687$(document).on("click", ".nav-medicines-view-more a", function() {
688    var button = $(this);
689    var elements = button.parentsUntil('ul').parent().find("li.showable");
690    var status = button.parent().attr('data-toggle');
691
692    elements.toArray();
693
694    if (status == "show") {
695        button.parent().attr("data-toggle", "hide");
696        button.find(".view-more").addClass("dnone");
697        button.find(".view-less").removeClass("dnone");
698
699        elements.each(function(i, el) {
700            var element_fade = setTimeout(function() {
701                $(el).fadeIn();
702            }, 10 * i);
703        });
704    } else {
705        button.parent().attr("data-toggle", "show");
706        button.find(".view-more").removeClass("dnone");
707        button.find(".view-less").addClass("dnone");
708
709        elements.get().reverse();
710
711        elements.each(function(i, el) {
712            var element_fade = setTimeout(function() {
713                $(el).fadeOut();
714            }, 10 * i);
715        });
716    }
717
718    return false;
719});
720
721$('body').on('focus', '.product-quantity-input', function () {
722    $(this).data('prev-value', $(this).val());
723});
724
725var fireOnchangeProductQuantity = function() {
726
727    //enter on some browsers exec value change on input, prevent that
728    $('.product-quantity-input').on('keydown', function(e) {
729        if (e.key === 'Enter') {
730            e.preventDefault();
731            debouncedProductQuantityChange($(this));
732        }
733    });
734
735    // on change qtt writing input
736    $('.product-quantity-input').on('change blur paste', function() {
737        debouncedProductQuantityChange($(this));
738    });
739
740    $('.layette_quantity_input').on('change blur paste', function() {
741        layetteChangeInput($(this));
742    });
743
744    $(".promo-question-text").click(function () {
745        $(".block-check-promo").toggle();
746    });
747};
748
749// Top bar search, filter by category
750$(document).on("click", ".ecommerce-search-bar ul li a", function(e) {
751    e.preventDefault();
752    $(".searching-category-selection-text").html("<span class='category-label truncate'>" + $(this).html() + "</span>");
753    $(".searching-category").val(($(this).attr("data-category")));
754});
755
756$(document).on("focus", ".searching", function() {
757    $(this).select();
758});
759
760function productQuantityChange($input) {
761    var quantity = $input.val(),
762        product_id = $input.attr('data-productid'),
763        // Sense axis_value_id el backend no troba la linia de la variant i el canvi queda
764        // en un no-op silencios (HTTP 200).
765        axis_value_id = $input.attr('data-axis-value-id'),
766        href = $input.attr('data-href'),
767        paymentRadio = $('input[name="payment_option"]:checked'),
768        dataPaymentMethod = $input.attr('data-payment-method') ?? 'euros',
769        payment_method = dataPaymentMethod ? dataPaymentMethod : (paymentRadio.length ? paymentRadio.val() : null),
770        prevVal = parseInt($input.data('prev-value')) || 1;
771
772    const currentVal = parseInt($input.val()) || 1;
773
774    // CHECK IF CAN CHANGE QUANTITY
775    if (dataPaymentMethod === 'points' && currentVal > prevVal) {
776        if (!inputChangeCanRedeemMorePoints($input)) {
777            quantity = $input.val(prevVal);  // previous value
778            return false;
779        }
780    }
781
782
783    //only allow numbers
784    if (typeof quantity === 'string') {
785        quantity = quantity.replace(/[^0-9]/g, "")
786    }
787    $input.val(quantity)
788
789    // only check change if input has value
790    if (!quantity) {
791        return false;
792    }
793
794    var url = href + '?product_id=' + product_id + '&quantity=' + quantity;
795    if (axis_value_id) {
796        url += '&axis_value_id=' + axis_value_id;
797    }
798    var lineKey = foLineKey(product_id, axis_value_id);
799    $.ajax({
800        url: url,
801        data: {
802            'payment_method': payment_method
803        },
804        method: 'post',
805        beforeSend: function () {
806            //buttonSelector.toggleClass('hide');
807        },
808        success: function (data) {
809            $('#cart-error').hide();
810            $('#stock-warning-' + lineKey).hide();
811
812            if (typeof data['error'] === "undefined") {
813                $('#cart-error-'+ lineKey).hide();
814            } else {
815                $('#cart-error-'+ lineKey).show();
816                $('#cart-error-'+ lineKey).removeClass('hide');
817                $('#cart-error-'+ lineKey + ' span').html(data['error']);
818            }
819
820            // Clean previous max amount alert and re-evaluate
821            $('#cart-max-amount-alert').remove();
822
823            // Show/hide max amount warning and disable/enable payment buttons
824            if (data.exceeds_max_amount && data.max_amount_error_message) {
825                $('.cart-summary .btn-layette-content').first().before(
826                    '<div class="alert alert-danger" id="cart-max-amount-alert" style="margin-bottom:10px;">' +
827                    '<i class="fa fa-exclamation-triangle"></i> ' + data.max_amount_error_message +
828                    '</div>'
829                );
830                $('.payment-cart-button').prop('disabled', true).addClass('disabled');
831            } else {
832                $('.payment-cart-button').prop('disabled', false).removeClass('disabled');
833            }
834
835            updateQuantityInput(data, payment_method, product_id);
836
837            $('.cart-count').html('(' + data['count'] + ')');
838            $('#sticky-Cart-Count').html(data['cartInfo']);
839            //new
840            $('.count-product-carts').html(data['count']);
841
842            //used in products cart
843            if(data['count'] > 1 || data['count'] === 0){
844                $('.articles-count-plural').show();
845                $('.articles-count-singular').hide();
846            }else{
847                $('.articles-count-singular').show();
848                $('.articles-count-plural').hide();
849            }
850
851            //modal
852            $('#total-cart-products').html(data['count']);
853            $('#product-quantity').html(quantity+' x ');
854            $('#total-price-cart').html(data.subtotal + '€').fadeIn();
855
856            if (data['product_offer'] != null && data['product_price_quantity_before_offer'] != data['product_price_quantity']) {
857                $('#product-regular-price').fadeIn();
858                $('#product-regular-price').html(data['product_price_quantity_before_offer']+ '€');
859            } else {
860                $('#product-regular-price').fadeOut();
861            }
862
863            $('#product-price').html(data['product_price_quantity']+ '€').fadeIn();
864
865            //online shop show
866            $('#product-qtt-span').html('');
867            $('#product-qtt-span').html(quantity);
868
869            // si tenim descompte/oferta aplicada, mostrem preu tatxat(preu previ x quantitat) amb info corresponent
870            // Guard: si la resposta no porta el camp (p.ex. URL/resposta inesperada), no petar amb .length d'undefined.
871            if (data['product_price_quantity_before_offer'] && data['product_price_quantity_before_offer'].length) {
872                var $regularPriceElement = $('#price-total-'+lineKey+'-regular-price');
873                var payment_method = data.payment_method || 'euros';
874                var $priceContainer = $('#price-total-'+lineKey+'-'+payment_method).parent();
875
876                // Use line_has_discount to determine if we should show the striked price
877                // This correctly handles discounts that only apply from 2+ units
878                if (data['line_has_discount']) {
879                    // If element doesn't exist, create it
880                    if ($regularPriceElement.length === 0) {
881                        var regularPriceHtml = '<h4 id="price-total-'+lineKey+'-regular-price" ' +
882                            'style="font-size: 13px; text-decoration: line-through; color: #B1B1B1; padding: 0 .2rem;">' +
883                            data['product_price_quantity_before_offer']+ '€' +
884                            '</h4>';
885
886                        // Insert before the main price element
887                        $('#price-total-'+lineKey+'-'+payment_method).before(regularPriceHtml);
888                    } else {
889                        // Update and show existing element
890                        $regularPriceElement.html(data['product_price_quantity_before_offer']+ '€');
891                        $regularPriceElement.show();
892                    }
893                } else {
894                    // Hide if exists, or do nothing if it doesn't
895                    if ($regularPriceElement.length > 0) {
896                        $regularPriceElement.hide();
897                    }
898                }
899            }
900
901            // get offer info
902            if (data['product_offer'] != null) {
903
904                quantity_module = data['product_offer']['apply_units'] - (quantity % data['product_offer']['apply_units']);
905
906                if ($("#product-offer-" + product_id + " .offer-product-remaining").hasClass('tooltip-message')) {
907                    $("#product-offer-" + product_id + " .offer-product-applied-tooltip").hide();
908                }
909
910                if (quantity_module == data['product_offer']['apply_units']) {
911                    if ($("#product-offer-" + product_id + " .offer-product-remaining").hasClass('tooltip-message')) {
912                        $("#product-offer-" + product_id + " .offer-product-applied-tooltip").show();
913                    }
914                    $("#product-offer-" + product_id + " .offer-product-remaining").hide();
915                } else {
916                    $("#product-offer-" + product_id + " .offer-product-remaining big").html(quantity_module);
917                    $("#product-offer-" + product_id + " .offer-product-remaining").show();
918
919                    if (quantity_module == 1) {
920                        $("#product-offer-" + product_id + " .offer-product-remaining-plural").hide();
921                        $("#product-offer-" + product_id + " .offer-product-remaining-singular").show();
922                    } else {
923                        $("#product-offer-" + product_id + " .offer-product-remaining-singular").hide();
924                        $("#product-offer-" + product_id + " .offer-product-remaining-plural").show();
925                    }
926                }
927
928                $(".modal-product-offer").html(data['product_offer_widget']);
929            }
930
931            let line = null;
932
933            if (Array.isArray(data.lines)) {
934                line = data.lines.find(function(l) {
935                    return l.content && String(l.content.id) === String(product_id)
936                        && (
937                            payment_method
938                                ? l.payment_method === payment_method
939                                : true
940                        );
941                });
942
943                // FARMAPREMIUM POINTS TOTAL (ORISSENS)
944                if (data.points) {
945                    $(summaryAccumulatedPoints).html('+'+formatPoints(data.points.accumulated_points));
946                    $(summaryRedeemedPoints).html('-'+formatPoints(data.points.redeemed_points));
947                    $(summaryTotalPoints).html(formatPoints(data.points.total_points));
948                    $(summaryRemainingPoints).html(formatPoints(data.points.remaining_points));
949                }
950            }
951
952            renderProductPoints(data, line, product_id, payment_method);
953
954            $('#product-quantity-'+product_id+'').html('x '+quantity);
955
956            //APPLY DISCOUNT SUMMARY LINES
957            getDiscountLines(data)
958
959            //DATA SUBTOTAL NOT USED ANYMORE, getDiscountLines -> subtotalWithoutDiscount
960            //$('.layette-total-net').html(data.subtotal + '€').fadeIn();
961            $(classUpdateShippingAmountCheckbox).html(formatCurrency(data.shipping_amount)).fadeIn();
962            $('.shipping-amount-delivery').html(formatCurrency(data.shipping_amount_delivery)).fadeIn();
963
964            $(classUpdateTotalBeforeDiscount).html(data.totalBeforeDiscount + '€').fadeIn();
965
966            $('.layette-total-net-shipping').html(formatCurrency(data.total)).fadeIn();
967
968            //left free shipping (minShipping - subtotal)
969            getLeftFreeShipping(data['freeShippingLeft']);
970
971            //todo sum prices + shipment
972            if (data == 0) {
973                $('#layette-custom-actions').fadeOut();
974                $('.stickyCartButton').fadeOut();
975            }
976        },
977        error: function(data) {
978
979            var jsonResponse = $.parseJSON(data.responseText);
980
981            $('.product-quantity-input[data-productid="' + jsonResponse.productId + '"]').val(jsonResponse.count);
982            $('#product-quantity').html(jsonResponse.quantityLimit + ' x ');
983
984            $('#cart-error').show();
985            $('#cart-error').removeClass('hide');
986            $('#cart-error span').html(jsonResponse.error);
987
988            // lineKey i no jsonResponse.productId: la resposta d'error no porta l'eix.
989            $('#cart-error-'+ lineKey).show();
990            $('#cart-error-'+ lineKey).removeClass('hide');
991            $('#cart-error-'+ lineKey + ' span').html(jsonResponse.error);
992
993            return false;
994        }
995    });
996}
997
998// debounce on input change (avoid multiple requests if user change input more than one time)
999const debouncedProductQuantityChange = debounce(function($input) {
1000    productQuantityChange($input);
1001}, 400);
1002
1003function layetteChangeInput($input) {
1004    var $this = $input;
1005    var value = $this.val().replace(/[^0-9]/g, '');
1006    var max = parseInt($this.attr('max'));
1007    var limit_stock = $this.attr("limit-stock");
1008
1009    if (parseInt(value) > max && limit_stock) {
1010        value = max>0?max:1;
1011    } else if (parseInt(value) < 1) {
1012        value = 1;
1013    }
1014    $this.val(value);
1015}
1016
1017function getDiscountLines (data) {
1018
1019    //SUBTOTAL WITHOUT DISCOUNT -> NOW WE ALWAYS USE THIS ONE AND SHOW DISCOUNTS AFTER
1020    $('#layette-total-net-no-discount').html(formatCurrency(data['subtotal']));
1021
1022    // descompte total carret (INCLOU SUMA TOTS DESCOMPTES/OFERTES)
1023    $('#cart-total-discount').html('-'+formatCurrency(data['discount']));
1024
1025    // hide discount parent line if it is 0
1026    if(data['discount'] === 0) {
1027        $('#cart-total-discount').parent().hide();
1028        $(noDiscountTotalsSummary).show();
1029        $(discountTotalsSummary).hide();
1030    } else {
1031        $('#cart-total-discount').parent().show();
1032        $(discountTotalsSummary).show();
1033        $(noDiscountTotalsSummary).hide();
1034    }
1035
1036    if (data['discount'] === 0) {
1037        $(totalBeforeDiscountLoggedFP).hide();
1038        $(discountBoxSummary).hide();
1039    } else {
1040        $(totalBeforeDiscountLoggedFP).show();
1041        $(discountBoxSummary).show();
1042    }
1043
1044    // descompte potencial farmapremium NO LOGIN - NO APLICA INFORMATIU
1045    $('#farmapremium-total-discount-message').html(data['discount_farmapremium_message']);
1046    $('#price-total-without-fp').html(data['total_without_farmapremium']+ '€');
1047
1048    // Show/hide potential discount container based on message content
1049    if (data['discount_farmapremium_message'] && data['discount_farmapremium_message'].length > 0) {
1050        $('.potential-discount-container').show();
1051    } else {
1052        $('.potential-discount-container').hide();
1053    }
1054
1055    // Punto único: con CUALQUIER cambio de carrito (añadir, cantidad, eliminar, canje) que
1056    // devuelva el estado del canje FP, reajusta el tope del slider (BUG-655).
1057    if (typeof fpApplyRedeemState === 'function' && typeof data !== 'undefined' && typeof data.fpRedeem !== 'undefined') {
1058        fpApplyRedeemState(data.fpRedeem);
1059    }
1060}
1061
1062function setRenderCartSectionsFromController (data) {
1063
1064    // get previous form values from form
1065    let previousFormValues = {};
1066    $('#cart-address-collapse :input').each(function () {
1067        previousFormValues[this.name] = $(this).val();
1068    });
1069    let previousTownId = $('#town_id').val();
1070    let previousTownName = $('#town_id').find('option:selected').text();
1071
1072    $('.cart-summary-fixed').html(data.htmlCart_fixed);
1073    $('.cart-summary-fixed').removeClass('hide');
1074    $('.cart-summary-fixed').show();
1075
1076    if (data.htmlCart_content) {
1077        $('.cart-container').html(data.htmlCart_content);
1078        jQuery('<div class="quantity-nav"><button class="quantity-button quantity-up"><i class="fa fa-chevron-up"></i></button><button class="quantity-button quantity-down"><i class="fa fa-chevron-down"></i></button></div>').insertAfter('.quantity input');
1079        $('[data-toggle="tooltip"]').tooltip();
1080        fireOnchangeProductQuantity();
1081    }
1082
1083    // set previous form values before action (inputs)
1084    for (let inputName in previousFormValues) {
1085        $('[name="'+inputName+'"]').val(previousFormValues[inputName]);
1086    }
1087
1088    if(data.js_content) {
1089        $('.js-section').html(data.js_content);
1090    }
1091
1092    // Restore select2 value (poblacion)
1093    if (previousTownId) {
1094        let $town = $('#town_id');
1095        // Check if the option already exists
1096        if ($town.find('option[value="' + previousTownId + '"]').length) {
1097            $town.val(previousTownId).trigger('change');
1098        } else {
1099            // If the option does not exist, create it
1100            let newOption = new Option(previousTownName, previousTownId, true, true);
1101            $town.append(newOption).trigger('change');
1102        }
1103    }
1104
1105}
1106
1107function updateCartModal(data) {
1108    $('.cart-count').html('(' + data.count + ')');
1109    $('#cart-image-product')
1110        .html(data.product_image)
1111        .attr('data-href', data.product_slug);
1112    $('#cart-image-name')
1113        .html(data.product_name)
1114        .attr('data-href', data.product_slug);
1115    $('#product-quantity').html(data.product_quantity + ' x');
1116    $('.modal-product-offer').html(data.product_offer_widget);
1117    $('#total-cart-products').html(data.count);
1118
1119    // Update free shipping left in modal
1120    if (typeof data.freeShippingLeft !== 'undefined') {
1121        getLeftFreeShipping(data.freeShippingLeft);
1122    }
1123}
1124
1125
1126function updateQuantityInput(data, payment_method_selected = 'euros', selected_product_id = null) {
1127    var productId = selected_product_id ?? data.product_id;
1128    if (!productId && Array.isArray(data.lines) && data.lines.length === 1) {
1129        productId = data.lines[0].content.id;
1130    }
1131
1132    // 1. Busca la línea del producto y método de pago seleccionado
1133    if (payment_method_selected === null) {
1134        payment_method_selected = 'euros';
1135    }
1136
1137    var line = null;
1138    if (Array.isArray(data.lines)) {
1139        line = data.lines.find(function(l) {
1140            return l.content && String(l.content.id) === String(productId)
1141                && (
1142                    payment_method_selected
1143                        ? l.payment_method === payment_method_selected
1144                        : true
1145                );
1146        });
1147    }
1148
1149    // 2. Busca el input que coincide con el producto y el método de pago
1150    var selector = '.product-quantity-input[data-productid="' + productId + '"]';
1151    // Amb variants hi ha diversos inputs amb el mateix data-productid: sense filtrar per
1152    // l'eix, escrivim la quantitat d'una linia a l'altra.
1153    var axisValueId = (line && line.axis_value_id) || data.axis_value_id;
1154    if (axisValueId) {
1155        selector += '[data-axis-value-id="' + axisValueId + '"]';
1156    }
1157    // TODO check
1158    if (line && line.payment_method) {
1159        selector += '[data-payment-method="' + line.payment_method + '"]';
1160    } else if (payment_method_selected) {
1161        selector += '[data-payment-method="' + payment_method_selected + '"]';
1162    }
1163    var $input = $(selector);
1164    if ($input.length === 0) {
1165        // fallback: busca solo por product_id (+ eix)
1166        var fallbackSelector = '.product-quantity-input[data-productid="' + productId + '"]';
1167        if (axisValueId) {
1168            fallbackSelector += '[data-axis-value-id="' + axisValueId + '"]';
1169        }
1170        $input = $(fallbackSelector).first();
1171    }
1172
1173
1174    // 3. Detecta el valor de remaining_points
1175    var remainingPoints = '';
1176    if (data.points && typeof data.points.remaining_points !== 'undefined') {
1177        remainingPoints = data.points.remaining_points;
1178    } else if (typeof data.remaining_points !== 'undefined') {
1179        remainingPoints = data.remaining_points;
1180    } else if (line && typeof line.remaining_points !== 'undefined') {
1181        remainingPoints = line.remaining_points;
1182    }
1183
1184    // 4. Actualiza el input solo si lo encuentra
1185    if ($input.length > 0) {
1186        if (line) {
1187            $input.attr('data-redemption-points', line.redemption_points || '');
1188            $input.attr('data-payment-method', line.payment_method || '');
1189            $input.attr('data-remaining-points', remainingPoints !== '' ? remainingPoints : '');
1190            $input.attr('data-quantity', line.quantity || 1);
1191            $input.val(line.quantity || 1);
1192        } else {
1193            $input.attr('data-redemption-points', data.redemption_points || '');
1194            $input.attr('data-payment-method', data.payment_method || '');
1195            $input.attr('data-remaining-points', remainingPoints !== '' ? remainingPoints : '');
1196            $input.attr('data-quantity', data.product_quantity || 1);
1197            $input.val(data.product_quantity || 1);
1198
1199            if(remainingPoints && $(productShowRemainingUserPoints).length) {
1200                $(productShowRemainingUserPoints).html(formatPoints(remainingPoints));
1201            }
1202
1203            // Productr show -> dont allow to add again if user points are not enough
1204            if ($(productShowInputPointsSelected).length) {
1205
1206                if ( $(productShowInputPointsSelected).attr('data-product-points') > remainingPoints) {
1207                    // disable points option
1208                    $(productShowInputPointsSelected).prop("disabled", true);
1209
1210                    // select euros option
1211                    var paymentOptionEurosNotDisabled = $('.payment-option-radio:not(:disabled)').first();
1212
1213                    if (paymentOptionEurosNotDisabled.length) {
1214                        paymentOptionEurosNotDisabled.prop('checked', true);
1215                    }
1216                } else {
1217                    $(productShowInputPointsSelected).prop("disabled", false);
1218                }
1219
1220            }
1221        }
1222
1223    }
1224}
1225
1226// function used on address options (no transport(recogida) / click and collect / home_delivery)
1227function sendDeliveryOptionRequest($element, extraData = {}) {
1228    const url = $element.attr('data-url');
1229    const pharmacyId = $element.attr('data-pharmacy-id');
1230    const farmapremiumLogin = $element.attr('data-farmapremium');
1231    const $button = $element.find('[type=submit]').last();
1232
1233    $.ajax({
1234        type: "POST",
1235        url: url,
1236        cache: false,
1237        data: {
1238            pharmacyId,
1239            farmapremiumLogin,
1240            ...extraData
1241        },
1242        dataType: 'json',
1243        beforeSend: function () {
1244            $button.hide();
1245            $button.after(`
1246                <div class="cssload-square">
1247                    <div class="cssload-square-part cssload-square-green"></div>
1248                    <div class="cssload-square-part cssload-square-pink"></div>
1249                    <div class="cssload-square-blend"></div>
1250                </div>
1251            `);
1252        },
1253        success: function (data) {
1254            setRenderCartSectionsFromController(data);
1255        },
1256        error: function (thrownError) {
1257            $('#cart-error').removeClass('hidden').html(thrownError.error || 'Error');
1258        },
1259        complete: function () {
1260            $('.cssload-square').remove();
1261            $button.show();
1262            $('#summary-message-code').hide().fadeIn();
1263        }
1264    });
1265}
1266
1267
1268function renderProductPoints(data, line = null, product_id = null, payment_method = null) {
1269
1270    let lineKey = foLineKey(product_id, (line && line.axis_value_id) || data.axis_value_id);
1271
1272    let modal_product_price = $('#product-price');
1273    let modal_product_regular_price = $('#product-regular-price');
1274    let modal_product_unitary_price = $('#product-price-unitary');
1275    let modal_total_price = $('#total-price-cart');
1276
1277    let redemption_points = Number(data.redemption_points);
1278    let product_quantity = Number(data.product_quantity);
1279    let points_spent = data.points_spent;
1280    let product_price_quantity = data.product_price_quantity;
1281    let payment_method_final = data.payment_method;
1282
1283    if (line) {
1284        redemption_points = Number(line.redemption_points);
1285        product_quantity = Number(line.quantity);
1286        points_spent = Number(line.points_spent);
1287        product_price_quantity = line.total_line;
1288        payment_method_final = line.payment_method;
1289    }
1290
1291    let puntosLinea = redemption_points * product_quantity;
1292    if (Number(points_spent) > 0) puntosLinea = Number(points_spent);
1293
1294    if (payment_method_final === 'points') {
1295        modal_product_price.html(formatPoints(puntosLinea));
1296        modal_product_regular_price.html('');
1297        modal_product_unitary_price.html(formatPoints(redemption_points));
1298        modal_total_price.html(formatPoints(puntosLinea));
1299
1300        if (product_id && payment_method) {
1301            $('#price-total-' + lineKey + '-' + payment_method).html(formatPoints(redemption_points * product_quantity));
1302        }
1303    } else {
1304        let shouldShowOffer = data.product_price_quantity_before_offer != data.product_price_quantity_after_offer;
1305        modal_product_price.html(data.product_price_quantity_after_offer + '€');
1306        modal_product_regular_price.html(shouldShowOffer ? data.product_price_quantity_before_offer + '€' : '');
1307        modal_product_unitary_price.html(data.product_price_unitary + '€');
1308        modal_total_price.html(data.total + '€');
1309
1310        if (product_id && payment_method) {
1311            $('#price-total-' + lineKey + '-' + payment_method).html(formatCurrency(product_price_quantity));
1312        }
1313    }
1314}
1315
1316
1317
1318function formatCurrency(value) {
1319    let number = parseFloat(value);
1320    if (isNaN(number)) {
1321        return '0,00€'; // Si el valor no es un número válido, devuelve 0€
1322    }
1323    return number.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + '€';
1324}
1325
1326function inputChangeCanRedeemMorePoints($input) {
1327    const redemptionPoints = Number($input.attr('data-redemption-points')) || 0;
1328    const remainingPoints = Number($input.attr('data-remaining-points')) || 0;
1329    const paymentMethod = $input.attr('data-payment-method') ?? 'euros';
1330    const quantity = parseInt($input.val()) || 1;
1331
1332    if (paymentMethod === 'points' && remainingPoints < redemptionPoints) {
1333        alert('No tienes suficientes puntos para añadir más unidades de este producto.');
1334        return false;
1335    }
1336    return true;
1337}
1338
1339function updateQuantity($input, newVal) {
1340    $input.val(newVal);
1341    $input.data('prev-value', newVal);
1342    productQuantityChange($input);
1343}
1344
1345function debounce(func, wait, immediate) {
1346    var timeout;
1347    return function() {
1348        var context = this,
1349            args = arguments;
1350        var later = function() {
1351            timeout = null;
1352            if (!immediate) func.apply(context, args);
1353        };
1354        var callNow = immediate && !timeout;
1355        clearTimeout(timeout);
1356        timeout = setTimeout(later, wait);
1357        if (callNow) func.apply(context, args);
1358    };
1359};

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.