PageSourceSearch

https://b2bshop.smartdk.com/common_cocoon/js/site.js?2026100805

js smartdk.com collected 2026-10-08 05:09:42 UTC 229,627 bytes, 6,017 lines download raw bytes

1// ---- VARIABLES ---- //
2const r = $(":root");
3const onSurface = getComputedStyle(document.documentElement).getPropertyValue("--onSurface");
4const TopMenuPos = $(".navbar").position().top;
5const currentCategory = $(".categoryProducts")?.data("category");
6
7class CommentLine {
8    constructor(salesquoteid, lineid, lineno, masterdatacode, masterdatavalue, value, isFile, fileData) {
9        this.salesquoteid = salesquoteid;
10        this.lineid = lineid;
11        this.lineno = lineno;
12        this.masterdatacode = masterdatacode;
13        this.masterdatavalue = masterdatavalue;
14        this.value = value;
15        this.isFile = isFile;
16        this.fileData = fileData;
17    }
18}
19
20var page;
21var search_timer;
22var toast_interval;
23var logoSectionWidth;
24var extendedMainNavWidth;
25var clearTimeout_updateProduct;
26var searchParams;
27var mobile;
28var footerHeight;
29var sortingMethod;
30var defaultSortingMethod;
31var totalCustomerPages;
32var customerCount;
33var visibleCustomersList;
34var productJson;
35var screenWidth;
36
37// Endless scroll state for subcategory products
38var endlessScrollLoading = false;
39var endlessScrollCurrentPage = 1;
40var endlessScrollHasMore = true;
41var endlessScrollPageSize = 48;
42var endlessScrollTotalCount = 0;
43var endlessScrollCategoryNo = "";
44var endlessScrollMinVisibleProducts = 48; // Minimum products to show before auto-loading more when filtering
45
46var cookie_consent = false;
47var search_string = "";
48var CurrentCatID = "";
49var navbarExpanded = true;
50var collapseMainNav = false;
51var boot = true;
52var URIJoiner = "|:|";
53var URISplitter = "&&";
54var URIEquals = "::";
55var hasFired = false;
56var quickAddInitialized = false;
57var currentGalleryClass;
58var basketChangedTimeout = 300;
59var currentCustomerPage = 1;
60$.easing.myEasing = function (x, t, b, c, d) {
61    return -c * ((t = t / d - 1) * t * t * t - 1) + b;
62};
63
64var manyCategories = false;
65var multiLogos = "";
66var autoCollapseNav = true;
67var navCollapseWidth = 0;
68var sideNav = true;
69var sideNavRight = false;
70var sidebarEnabled = false;
71var sidebarCollapseWidth = 800;
72var hoverNavItems = false;
73var sideFilter = false;
74var sideFilterCollapseWidth = 1000;
75var galleryClasses = ["NormalView", "GridView", "ListView"];
76var defaultGalleryClass = "NormalView";
77var fullWidthRelatedItems = false;
78var horizontalRelatedItems = false;
79var customersPerPage = 200;
80var inventoryLocDisplay = "inline";
81
82$.urlParam = function (name) {
83    let results = new RegExp("[?&]" + name + "=([^&#]*)").exec(window.location.search);
84
85    return results !== null ? results[1] || 0 : false;
86};
87
88// ---- DOCUMENT READY ---- //
89$(function () {
90    //global functions
91    variablesInit();
92    lazyload();
93    MakeMenuStructure();
94    AddToCartBtn();
95    listenersInit();
96    ajaxQueueInit();
97    niceSelectInit();
98    searchSelectInit();
99    resizeNavbar($(window).scrollTop());
100    calculateScrollbarWidth();
101
102    //init pages
103    pageInit();
104
105    //gallery items
106    galleryItemsInit();
107
108    //variable related
109    bootDynamicAspects();
110
111    boot = false;
112
113    console.info("UNBOX-ECOMMERCE::V2.3.0.5::COCOON");
114});
115function galleryItemsInit() {
116    if ($("#sortDropdown").length) {
117        currentGalleryClass = defaultGalleryClass;
118        sortDropdownInit();
119        filterInit();
120        checkUrlParams();
121        CategoryPagerV2.init();
122    }
123    if ($(".sideControls, .sidebarEnabled").length) adjustSideControlsHeight();
124    if ($(".relatedProduct").length) {
125    }
126    if ($(".categoryList").length) {
127        $(".categoryList").addClass("containsAnyImages" + ($(".categoryList .containsImageTrue").length > 0 ? "True" : "False"));
128        $(".categoryList .containsImageTrue img").show();
129    }
130    // Initialize endless scroll for subcategory products
131    if ($("#subCatContainer").length) {
132        endlessScrollInit();
133    }
134    if ($("#GeneratedDealerList").length) {
135        $.ajax({
136            method: "GET",
137            url: "/shop/api/GetDealerList",
138            dataType: "json",
139            contentType: "application/json",
140        }).done(function (json) {
141            $("#GeneratedDealerList").html("");
142
143            json.sort(function (a, b) {
144                return a.name.localeCompare(b.name);
145            });
146
147            for (let i = 0; i < json.length; i++) {
148                let item = json[i];
149                $("#GeneratedDealerList").append(
150                    '<li class="GeneratedDealerListLi"><b>' +
151                        item.name +
152                        "</b><br>" +
153                        item.address +
154                        "<br>" +
155                        item.zipcode +
156                        " " +
157                        item.city +
158                        "<br><a class='phone' href='tel:" +
159                        item.phone +
160                        "'>" +
161                        item.phone +
162                        "</a><a class='location' href=\"https://www.google.dk/maps/dir/''/" +
163                        item.address +
164                        " " +
165                        item.zipcode +
166                        " " +
167                        item.city +
168                        '/">' +
169                        ShowTheWayDealerList +
170                        "</a><br></li>",
171                );
172            }
173        });
174    }
175}
176function bootDynamicAspects() {
177    if (sideNav) {
178        $("#main_nav").addClass("collapse-horizontal");
179        if (sideNavRight) $("#main_nav").addClass("horizontal-right");
180    }
181    if (hoverNavItems) $("body").addClass("hoverNav");
182    if (multiLogos != "") appendLogos();
183
184    if ($(".loggedIn").length) {
185        changeCustomerType();
186        changeAltDelivery();
187        if ($(".scShopCheckout").length) getBasketInfo();
188
189        setTimeout(function () {
190            $.ajax({
191                method: "GET",
192                url: "/api/basket",
193                cache: false,
194            }).done(function (data) {
195                if (data.success == false) {
196                    alert(data.message);
197                } else {
198                    parseBasket(data);
199                }
200            });
201        }, 500);
202
203        incDecInit();
204    }
205    if ($(".scShopCheckout").length) {
206        $(".addressSearch")
207            .autocomplete({
208                source: function (request, response) {
209                    $.ajax({
210                        url: "https://dawa.aws.dk/vejnavnpostnummerrelationer/autocomplete?q=" + request.term,
211                        dataType: "jsonp",
212                        success: function (data) {
213                            response(data);
214                        },
215                    });
216                },
217                minLength: 2,
218                select: function (event, ui) {
219                    $(".formZipcode").val(ui.item.vejnavnpostnummerrelation.postnr);
220                    $(".formCity").val(ui.item.vejnavnpostnummerrelation.postnrnavn);
221                    //setTimeout(function () {
222                    $(".formAddress").val(ui.item.vejnavnpostnummerrelation.vejnavn + " ");
223                    //}, 400);
224                    setBasketInfo();
225                    return false;
226                },
227                change: function (event, ui) {},
228            })
229            .autocomplete("instance")._renderItem = function (ul, item) {
230            item = item.tekst;
231            return $("<li>").append(item).appendTo(ul);
232        };
233    }
234
235    $('input[name="quicksku"]').trigger("focus");
236
237    if ($(".pageViewPaymentHandlingPage").length != 0) {
238        //Are we validating a payment?
239        if (validatingpayment === true) {
240            checkIfPaymentIsComplete();
241        }
242    }
243}
244
245// --- PAGE INITIALIZATIONS --- //
246function pageInit() {
247    //global conditional functions
248    if (window.self.location.host !== window.top.location.host) {
249        window.top.location.href = window.self.location.href.split("#")[0];
250    }
251    if (cookie_consent != "") {
252        $("#cookieNotice").css("display", "none");
253    } else {
254        $("#cookieNotice").css("display", "block");
255    }
256
257    bannerInit();
258
259    //page specific functions
260    page = $("body").attr("class").split(" ")[0].split("pageView")[1].toLowerCase();
261
262    if (page == "product") {
263        productPageInit();
264    } else if (page == "basket") {
265        basketPageInit();
266    } else if (page == "checkout") {
267        checkoutPageInit();
268    } else if (page == "salesperson") {
269        salespersonPageInit();
270    } else if (page == "favorites") {
271        favoritesPageInit();
272    } else if (page == "account") {
273        accountPageInit();
274    } else if (page == "itemcompare") {
275        itemComparePageInit();
276    } else if (page == "page") {
277        pagePageInit();
278    }
279
280    onPageInit(page);
281}
282function bannerInit() {
283    const banners = $(".scBanner");
284    if (!banners.length) {
285        return;
286    }
287
288    // popups stay display:none until .show below, so the heading moves while still hidden
289    banners.filter(".scBanner-popup").each(function () {
290        moveBannerHeading($(this));
291    });
292
293    banners.addClass("show");
294
295    resizeNavBanners();
296    // an image finishing late changes the height the layout was measured from
297    $("#primary_nav .scBanner img").on("load", resizeNavBanners);
298
299    $(document).on("click", ".scBannerClose", function (e) {
300        e.preventDefault();
301        const banner = $(this).closest(".scBanner");
302        const layer = banner.closest(".scBannerLayer");
303        banner.removeClass("show").remove();
304        if (!layer.find(".scBanner").length) {
305            layer.remove();
306        }
307    });
308}
309// navigation banners are the only in-flow content of the fixed #primary_nav, so their measured
310// height is what --_topHeight is missing when it reserves room for the bars above the page
311function resizeNavBanners() {
312    let height = 0;
313
314    $("#primary_nav>.scBannerLayer").each(function () {
315        height += $(this).outerHeight(true);
316    });
317
318    $(":root").css("--navBannerHeight", height + "px");
319}
320// the header is rendered for every popup so it can hold the close button on its own. When the admin
321// marked a text block for it, that block is lifted out of the body flow and into the header.
322function moveBannerHeading(banner) {
323    const header = banner.children(".scBannerHeader");
324    const heading = banner.find('.scBannerContent>[block="text"][header]').first();
325    if (!header.length || !heading.length) {
326        return;
327    }
328
329    header.prepend(heading);
330}
331function checkoutPageInit() {
332    $(".niceSelect").niceSelect();
333    $(".searchSelect").searchSelect();
334    $(".checkoutDiv input").on("input paste change", function () {
335        $(this).removeClass("form-error");
336    });
337    $(".checkoutDiv select").on("change", function () {
338        $(this).removeClass("form-error");
339    });
340    $(".amountINPUT").each(function () {
341        let UOMCode = "";
342        if ($(this).find(".UnitOfMeasureCode").html()) {
343            UOMCode = $(this).find(".UnitOfMeasureCode").html();
344        }
345        $(this).html($(this).find("input").val() + " " + UOMCode);
346    });
347    $(".basketTableSection :is(.actions, .actions_record_delete)").remove();
348    $(".orderInfoRow:has([required])").addClass("required").find("label").append(requiredText);
349    if ($(".alert.alert-danger").length) {
350        $([document.documentElement, document.body]).animate(
351            {
352                scrollTop: $(".alert.alert-danger").first().offset().top,
353            },
354            300,
355        );
356    }
357    setInterval(
358        function () {
359            document.location.href = document.location.href;
360        },
361        1000 * 60 * 60,
362    );
363}
364function salespersonPageInit() {
365    $(".salesCustomerSearch")
366        .val("")
367        .on("input", debounce(customerSearch, 300))
368        .on("keydown", function (e) {
369            if (e.keyCode === 27) {
370                $(this).val("").trigger("blur");
371                e.stopPropagation();
372                return;
373            }
374        });
375    if ($(".tab-control").length) {
376        $(".tab-control .btn").on("click", function () {
377            $(this).addClass("active").siblings().removeClass("active");
378        });
379    }
380
381    $(".salesCustomerList tr[data-customer]:not(.blocked)").on("click", function () {
382        if ($(this).hasClass("active")) return;
383        $("body").addClass("quoteActive");
384        showDrawer($(this));
385    });
386    $(".pageSelect .first-page").on("click", function () {
387        currentCustomerPage = 1;
388        updatePage(false, $(this).parent().attr("class").includes("bottom"));
389    });
390    $(".pageSelect .prev-page").on("click", function () {
391        currentCustomerPage--;
392        updatePage(false, $(this).parent().attr("class").includes("bottom"));
393    });
394    $(".pageSelect .next-page").on("click", function () {
395        currentCustomerPage++;
396        updatePage(false, $(this).parent().attr("class").includes("bottom"));
397    });
398    $(".pageSelect .last-page").on("click", function () {
399        currentCustomerPage = totalCustomerPages;
400        updatePage(false, $(this).parent().attr("class").includes("bottom"));
401    });
402    $(".pageSelect input").on("change", function () {
403        if ($(this).val() > $(this).attr("max")) {
404            $(this).val($(this).attr("max"));
405        }
406        if ($(this).val() < 1) {
407            $(this).val($(this).attr("min"));
408        }
409        currentCustomerPage = $(this).val();
410        updatePage();
411    });
412    $("input.rowsPerPage")
413        .val(customersPerPage)
414        .on("keyup", function (e) {
415            //enter key
416            if (e.keyCode === 13) {
417                $(this).trigger("blur");
418            }
419        })
420        .on("blur", function () {
421            if ($(this).val() > 1000) $(this).val(1000);
422            customersPerPage = parseInt($(this).val());
423            totalCustomerPages = null;
424            currentCustomerPage = 1;
425            updatePage(true);
426        });
427    updatePage();
428}
429function accountPageInit() {
430    //For navigation
431    switch ($.urlParam("type")) {
432        case "":
433            $('.pageViewAccount .leftMenu option[value$="/"]').prop("selected", true);
434            break;
435        case "open":
436            $('.pageViewAccount .leftMenu option[value$="open"]').prop("selected", true);
437            break;
438        case "invoice":
439            $('.pageViewAccount .leftMenu option[value$="invoice"]').prop("selected", true);
440            break;
441        case "creditmemo":
442            $('.pageViewAccount .leftMenu option[value$="creditmemo"]').prop("selected", true);
443            break;
444    }
445}
446function toggleInventoryStatusNotAvailable(inventoryClass) {
447    $('span[replacekey="GetInventoryStatusText"]').toggleClass("notAvailable", inventoryClass == "outOfStock");
448}
449function productPageInit() {
450    if ($(".ProductMatrixTable").length & $('*[replacekey="GetProductDescription"]').length) {
451        let ProdDescItem = $('*[replacekey="GetProductDescription"]');
452    }
453    if (fullWidthRelatedItems) $(".ProductRelated").addClass("fullWidth");
454    if (horizontalRelatedItems) $(".ProductRelated").addClass("horizontal");
455
456    $.ajax({
457        method: "GET",
458        url: "/shop/api/GetProduct?productId=" + $('span[ReplaceKey="GetNo"]').text() + "&$expand=variants",
459        cache: false,
460    }).done(function (data) {
461        productJson = data;
462        ////SIMPLE VARIANTS//
463        $(".ProductBCVariantDropDown .itemVariantId option").each(function () {
464            elm = productJson.value.variantsBc.find((o) => o.no == $(this).val());
465            $(this).attr("data-priceTag", elm.priceTag);
466            if (elm.inventory.toString() != "") {
467                $(this).attr("data-stock", " (" + elm.inventory + ")");
468            } else {
469                $(this).attr("data-stock", "");
470            }
471
472            $(this).attr("data-stocktext", elm.inventoryStatusText);
473        });
474        $("select.niceSelect").niceSelect();
475        $(".nice-select .option").each(function () {
476            $(this).attr("title", $(this).find(".valuetext").html());
477        });
478        $("select.niceSelect:not(.newSelect)").on("change", function () {
479            elm = productJson.value.variantsBc.find((o) => o.no == $(this).val());
480
481            $('span[replacekey="GetPriceTag"]').text(elm.priceTag);
482            $('span[replacekey="GetListPriceTag"]').text(elm.listPriceTag);
483            $('span[replacekey="GetPriceDiscountAmountTag"]').text(elm.priceDiscountAmountTag);
484            $('span[replacekey="GetPriceDiscountPcsTag"]').text(elm.listPriceTag);
485            $('span[replacekey="GetProductDescription"]').text(elm.productDescription);
486            $('span[replacekey="GetInventoryQty"]').text(elm.inventory);
487            $('span[replacekey="GetInventoryByLocation"]').text(elm.getInventoryByLocationHtml);
488            $('span[replacekey="GetGTIN"]').text(elm.gtin);
489            $('span[replacekey="GetTarif"]').text(elm.tarif);
490            $('span[replacekey="GetOrigin"]').text(elm.origin);
491            $('span[replacekey="GetInventoryStatusText"]').text(elm.inventoryStatusText);
492            toggleInventoryStatusNotAvailable(elm.inventoryClass);
493            $('span[replacekey="GetAvailabalilityHtml"]').html(elm.availabilityText);
494            $('span[replacekey="GetVariantCode"]').text(" (" + elm.variantCode + ")");
495        });
496    });
497
498    $(".available .inputWrapper").each(function () {
499        let addEvent = "event";
500        if ($(this).siblings(".outOfStock").length) {
501            $(this).find("input").attr("disabled", true);
502            addEvent = "";
503        }
504        $(this).prepend(`<div class="input-group-text ${addEvent}DecAmount">-</div>`);
505        $(this).append(`<div class="input-group-text ${addEvent}AddAmount">+</div>`);
506    });
507    $(".available, .notAvailable").on("click", function () {
508        $("#mainimage").removeClass("lazyload");
509        if ($(this).attr("large-image") != "") {
510            $("#mainimage").attr("src", $(this).attr("large-image"));
511        } else {
512            $("#mainimage").attr("src", $("#mainimage").data("src"));
513        }
514
515        clickElm = null;
516        if (productJson != null) {
517            //MDI
518            try {
519                let data = productJson.value.variantsMdi.find((o) => o.result.value.no == $(this).data("itemno"));
520                if (data != null) {
521                    clickElm = data.result.value;
522                }
523            } catch (error) {}
524            try {
525                let data = productJson.value.variantsMdi.find((o) => o.value.no == $(this).data("itemno"));
526                if (data != null) {
527                    clickElm = data.value;
528                }
529            } catch (error) {}
530            //BC Variants
531            if (productJson.value.variantsBc.find((o) => o.no == $(this).find(".ProductMatrixVariantId").val()) != null) {
532                clickElm = productJson.value.variantsBc.find((o) => o.no == $(this).find(".ProductMatrixVariantId").val());
533            }
534        }
535
536        if (clickElm != null) {
537            const priceTag = clickElm.priceTag != null && clickElm.priceTag.trim().length ? clickElm.priceTag : productJson.value.priceTag;
538            $('span[replacekey="GetPriceTag"]').text(priceTag).attr("data-price", priceTag);
539
540            const listPriceTag =
541                clickElm.listPriceTag != null && clickElm.listPriceTag.trim().length
542                    ? clickElm.listPriceTag
543                    : productJson.value.listPriceTag;
544            $('span[replacekey="GetListPriceTag"]').text(listPriceTag).attr("data-price", listPriceTag);
545
546            const discountAmountTag =
547                clickElm.priceDiscountAmountTag != null && clickElm.priceDiscountAmountTag.trim().length
548                    ? clickElm.priceDiscountAmountTag
549                    : productJson.value.priceDiscountAmountTag;
550            $('span[replacekey="GetPriceDiscountAmountTag"]').text(discountAmountTag).attr("data-price", discountAmountTag);
551
552            const discountPctTag =
553                clickElm.priceDiscountPctTag != null && clickElm.priceDiscountPctTag.trim().length
554                    ? clickElm.priceDiscountPctTag
555                    : productJson.value.priceDiscountPctTag;
556            $('span[replacekey="GetPriceDiscountPcsTag"]').text(discountPctTag).attr("data-price", discountPctTag);
557
558            if (clickElm.productDescription != null && clickElm.productDescription.trim().length) {
559                $('span[replacekey="GetProductDescription"]').html(clickElm.productDescription);
560            } else {
561                $('span[replacekey="GetProductDescription"]').html(productJson.value.productDescription);
562            }
563
564            if (clickElm.inventoryQty != null && clickElm.inventoryQty.trim().length) {
565                $('span[replacekey="GetInventoryQty"]').text(clickElm.inventoryQty);
566            } else {
567                $('span[replacekey="GetInventoryQty"]').text(productJson.value.inventoryQty);
568            }
569
570            if (clickElm.getInventoryByLocationHtml != null && clickElm.getInventoryByLocationHtml.trim().length) {
571                console.log(clickElm.getInventoryByLocationHtml);
572                $('div[replacekey="GetInventoryByLocation"]').html(clickElm.getInventoryByLocationHtml);
573            } else {
574                $('div[replacekey="GetInventoryByLocation"]').html(productJson.value.getInventoryByLocationHtml);
575            }
576
577            if (clickElm.gtin != null && clickElm.gtin.trim().length) {
578                $('span[replacekey="GetGTIN"]').text(clickElm.gtin);
579                $('span[replacekey="GetGTIN"]').closest(".d-none").removeClass("d-none");
580            } else {
581                $('span[replacekey="GetGTIN"]').text(productJson.value.gtin);
582                $('span[replacekey="GetGTIN"]').closest(".d-none").addClass("d-none");
583            }
584            if (clickElm.tarif != null && clickElm.tarif.trim().length) {
585                $('span[replacekey="GetTarif"]').text(clickElm.tarif);
586                $('span[replacekey="GetTarif"]').closest(".d-none").removeClass("d-none");
587            } else {
588                $('span[replacekey="GetTarif"]').text(productJson.value.tarif);
589                $('span[replacekey="GetTarif"]').closest(".d-none").addClass("d-none");
590            }
591            if (clickElm.origin != null && clickElm.origin.trim().length) {
592                $('span[replacekey="GetOrigin"]').text(clickElm.origin);
593                $('span[replacekey="GetOrigin"]').closest(".d-none").removeClass("d-none");
594            } else {
595                $('span[replacekey="GetOrigin"]').text(productJson.value.origin);
596                $('span[replacekey="GetOrigin"]').closest(".d-none").addClass("d-none");
597            }
598
599            if (clickElm.inventoryStatusText != null && clickElm.inventoryStatusText.trim().length) {
600                $('span[replacekey="GetInventoryStatusText"]').html(clickElm.inventoryStatusText);
601                toggleInventoryStatusNotAvailable(clickElm.inventoryClass);
602                $('span[replacekey="GetInventoryStatusText"]').closest(".d-none").removeClass("d-none");
603            } else {
604                $('span[replacekey="GetInventoryStatusText"]').html(productJson.value.inventoryStatusText);
605                toggleInventoryStatusNotAvailable(productJson.value.inventoryClass);
606                $('span[replacekey="GetInventoryStatusText"]').closest(".d-none").addClass("d-none");
607            }
608
609            //If we got specific information for this item, show it, else hide the line
610            if (clickElm.availabilityText != null && clickElm.availabilityText.trim().length) {
611                $('span[replacekey="GetAvailabalilityHtml"]').html(clickElm.availabilityText);
612                $('span[replacekey="GetAvailabalilityHtml"]').closest(".d-none").removeClass("d-none");
613            } else {
614                $('span[replacekey="GetAvailabalilityHtml"]').html("");
615                $('span[replacekey="GetAvailabalilityHtml"]').closest(".AvailabalilitySection").parent().addClass("d-none");
616            }
617
618            if (clickElm.specifications != null) {
619                $('span[replacekey="GetSpecifications"]').html(clickElm.specifications);
620            }
621
622            if (clickElm.no != null) {
623                if (clickElm.variantCode != null) {
624                    $('span[replacekey="GetVariantCode"]').html(" (" + clickElm.variantCode + ")");
625                } else {
626                    $('span[replacekey="GetNo"]').html(clickElm.no);
627                }
628            }
629
630            $(this).find(".ProductMatrixAmountInput").trigger("focus").trigger("select");
631        }
632    });
633
634    $(".ProductMatrixAmountInput")
635        .on("blur", function () {
636            var val = $(this).val();
637            var step = $(this).attr("step");
638            var min = $(this).attr("min");
639
640            if (!isNaN(step) && !isNaN(min) && !isNaN(val)) {
641                if (Number(val) < Number(min) && Number(val) != 0) {
642                    val = min;
643                } else {
644                    if (Number(step) != 1) {
645                        if (Number(val) % Number(step) != 0) {
646                            val = val - (Number(val) % Number(step));
647                        }
648                    }
649                }
650            }
651            $(this).val(val);
652        })
653        .on("input", function () {
654            checkAmountValidity(this);
655        });
656
657    $(".eventAddAmount").on("click", function () {
658        let elm = $(this).siblings(".ProductMatrixAmountInput");
659        let oldVal = parseInt(elm.val());
660        let min = parseInt(elm.attr("min"));
661        let max = parseInt(elm.attr("max"));
662        let step = parseInt(elm.attr("step"));
663        if (min && min > oldVal) {
664            elm.val(min);
665            return;
666        }
667        step = step ? step : 1;
668        newVal = Math.round(oldVal / step) * step;
669        if (newVal <= oldVal) newVal = newVal + step;
670        if (max && newVal > max) {
671            elm.val(max);
672            return;
673        }
674        elm.val(newVal);
675    });
676
677    $(".eventDecAmount").on("click", function () {
678        let elm = $(this).siblings(".ProductMatrixAmountInput");
679        let oldVal = parseInt(elm.val());
680        let min = parseInt(elm.attr("min"));
681        let max = parseInt(elm.attr("max"));
682        let step = parseInt(elm.attr("step"));
683        if (max && max < oldVal) {
684            elm.val(max);
685            return;
686        }
687        step = step ? step : 1;
688        newVal = Math.round(oldVal / step) * step;
689        if (newVal >= oldVal) newVal = newVal - step;
690        if (min && newVal < min) {
691            elm.val(0);
692            return;
693        }
694        elm.val(newVal);
695    });
696
697    $(".InventoryLocation").addClass(inventoryLocDisplay);
698
699    favoritesButtonInit();
700}
701function basketPageInit() {
702    $(".elasticTextarea").each(function () {
703        reElastic($(this));
704    });
705    setInterval(function () {
706        $(".elasticTextarea").each(function () {
707            reElastic($(this));
708        });
709    }, 1000);
710    $(".elasticTextarea").on("keyup, keydown keypress", function () {
711        reElastic($(this));
712    });
713    if ($(".salesQuoteSection").length) {
714        mutlipleSalesQuotesInit();
715    }
716    $(".priceUnit button").on("mouseup", function () {
717        quickaddtocart(this);
718    });
719    favoritesButtonInit();
720    setInterval(
721        function () {
722            document.location.href = document.location.href;
723        },
724        1000 * 60 * 60,
725    );
726}
727function favoritesPageInit() {
728    if ($(".createFavListButton").length) {
729        $(".createFavListButton").on("click", function () {
730            showFavsListForm(this);
731        });
732    }
733}
734function itemComparePageInit() {
735    itemCompareOverflow();
736}
737function pagePageInit() {
738    $("img:not([alt])").attr("alt", "User inserted mage without alt text - please contact site administrator");
739}
740function listenersInit() {
741    $(document)
742        .on("keyup", function (e) {
743            if (e.key === "Escape") {
744                $(".searchLiveResult").html("");
745                $("#searchLive").trigger("blur");
746            }
747        })
748        .on("click", function (e) {
749            if (e.target.id != "searchLive" && $(e.target).parents(".searchLiveResult").length == 0) {
750                $(".searchLiveResult").html("");
751                $("#searchLive").trigger("blur");
752            }
753            $(".errorOverlay").remove();
754            if (!$(e.target).closest(".topBarUserSection").length && $(".otherAccounts.show").length) {
755                $(".otherAccounts").removeClass("show");
756            }
757        })
758        .on("click", "[data-href]", function (e) {
759            const target = $(e.target);
760            if (target.attr("onclick") != null) return;
761            if (
762                target.parents().filter(function () {
763                    return $(this).attr("onclick") != null;
764                }).length
765            )
766                return;
767            if (
768                target.is(".confirm_edit") ||
769                target.is(".discard_edit") ||
770                target.is(".confirm_delete") ||
771                target.is(".cancel_delete") ||
772                target.closest("tr").is(".editing")
773            )
774                return;
775            if (target.hasClass("input-group-text")) return;
776            if (target.is("input")) return;
777            if (target.closest("label").length || target.is("label")) return;
778            if ($(this).find(".editing").length) return;
779            if (target.closest(".lotDropdown").length) return;
780            if (target.hasClass("favAdd") || target.hasClass("favAmount")) return;
781
782            const href = $(this).data("href");
783            location.href = href.includes("#") ? href.split("#")[0] : href;
784        });
785    $(window).on("scroll", function () {
786        let scroll = $(this).scrollTop();
787        if ($(".sideControls, .sidebarEnabled").length) adjustSideControlsHeight();
788        resizeNavbar(scroll);
789        if (TopMenuPos > $(window).scrollTop()) {
790            $(".minilogo img").css("opacity", ".2");
791        } else {
792            $(".minilogo img").css("opacity", "1");
793        }
794        onScroll(scroll);
795    });
796    $(window).on("resize", function () {
797        screenWidth = $(window).width();
798        if ($(".sideControls, .sidebarEnabled").length) adjustSideControlsHeight();
799        checkWidthDependantElements();
800        calculateScrollbarWidth();
801        resizeNavBanners();
802        resizeNavbar($(this).scrollTop());
803        onWindowResize(screenWidth);
804    });
805    $("body").on("keydown", function (e) {
806        if (e.shiftKey && e.ctrlKey && e.keyCode == 70) {
807            $(".form-control.search ").trigger("focus").select();
808        }
809    });
810    $(".lang-option").on("click", function () {
811        let val = $(this).find(".fflag").data("lang");
812
813        let date = new Date();
814        date.setTime(date.getTime() + 365 * 24 * 60 * 60 * 1000);
815
816        document.cookie = "_scLanguage=" + val + ";expires=" + date.toUTCString() + ";path=/";
817
818        location.reload();
819    });
820    $(".OpenClose").on("click", function () {
821        $(".AdminPanel").toggleClass("open");
822    });
823    $(".smallimage").on("click", function () {
824        $(".videoplayer").hide();
825        $(".videoplayer").html("");
826        $(".imagediv").show();
827
828        $("#mainimage").removeClass("lazyload");
829        if ($(this).attr("large-image") != "") {
830            $("#mainimage").attr("src", $(this).attr("large-image"));
831        }
832    });
833    $("#searchLive").on("focus", function () {
834        $("#searchLive").trigger("keyup");
835    });
836    $("#searchLive").on("keyup", function (e) {
837        // Handle enter key for navigation to full search
838        if (e.keyCode === 13) {
839            $(this).addClass('loading');
840            window.location.href = "/search?query=" + $(this).val();
841            return;
842        }
843
844        let val = $(this).val();
845
846        if (search_string == val.trim() && $(".searchLiveResult").html().length > 0) return;
847
848        search_string = val.trim();
849
850        clearTimeout(search_timer);
851
852        if (val.length < 2) {
853            $(".searchLiveResult").html("");
854            return;
855        }
856
857        search_timer = setTimeout(function () {
858            $.ajax({
859                method: "GET",
860                url: "/api/search/" + val,
861                cache: false,
862            }).done(function (data) {
863                $(".searchLiveResult").html("");
864
865                $(data).each(function (k, v) {
866                    let addToCartHtml = "";
867                    if (v.canBuy) {
868                        addToCartHtml =
869                            '<div class="quickSearchAdd">' +
870                            '<div class="input-group input-group-sm IncDecSection mini flex-nowrap"><div class="input-group-text decButton">-</div><input class="ProductAmountInput form-control incDecInput" type="text" data-decimals="0" value="' +
871                            v.minQty +
872                            '" min="' +
873                            v.minQty +
874                            '" step="' +
875                            v.colliQty +
876                            '"><div class="input-group-text incButton">+</div></div>' +
877                            '<button class="btn btn-success btn-sm quickSearchAddBtn" ' +
878                            'data-itemno="' +
879                            v.no +
880                            '" data-uom="' +
881                            v.uom +
882                            '"><i class="fas fa-shopping-basket"></i></button>' +
883                            "</div>";
884                    }
885
886                    $(".searchLiveResult").append(
887                        '<li class="quickSearchItem">' +
888                            '<a href="' +
889                            v.link +
890                            '" class="quickSearchLink">' +
891                            '<img width="40" src="' +
892                            v.image +
893                            '">' +
894                            '<div class="quickSearchInfo">' +
895                            '<div class="name">' +
896                            v.name +
897                            "</div>" +
898                            '<div class="price">' +
899                            v.price +
900                            "</div>" +
901                            "</div>" +
902                            "</a>" +
903                            addToCartHtml +
904                            "</li>",
905                    );
906                });
907            });
908        }, 200);
909    });
910    // Bind add-to-cart click handlers for quick search results
911    $(".searchLiveResult")
912        .on("click", ".quickSearchAddBtn", function (e) {
913            e.preventDefault();
914            e.stopPropagation();
915            let btn = $(this);
916            let itemNo = btn.data("itemno");
917            let uom = btn.data("uom");
918            let qty = btn.siblings(".IncDecSection").find("input").val() || 1;
919
920            btn.addClass("loading");
921
922            $.ajax({
923                method: "POST",
924                url: "/api/basket/add/" + encodeURIComponent(itemNo) + "/quantity/" + qty + "/uom/" + uom,
925                cache: false,
926                dataType: "json",
927                contentType: "application/json",
928            })
929                .done(function (data) {
930                    parseBasket(data);
931                    btn.removeClass("loading").addClass("success");
932                    setTimeout(function () {
933                        btn.removeClass("success");
934                    }, 2000);
935                })
936                .fail(function () {
937                    btn.removeClass("loading").addClass("error");
938                    setTimeout(function () {
939                        btn.removeClass("error");
940                    }, 2000);
941                });
942        })
943        // Prevent link navigation when clicking on the input
944        .on("click", ".quickSearchQty", function (e) {
945            e.preventDefault();
946            e.stopPropagation();
947        })
948        // Handle increment/decrement buttons
949        .on("click", ".incButton, .decButton", function (e) {
950            adjustValue(this, $(this).hasClass("incButton"));
951        })
952        .on("blur", ".ProductAmountInput", function () {
953            checkAmountValidity(this);
954        });
955    $("#CustomerType").on("change", function (e) {
956        changeCustomerType();
957    });
958    $("#AltDelivery").on("change", function (e) {
959        changeAltDelivery();
960    });
961    $(".navbar-toggler").on("click", function () {
962        $("body").removeClass("showSideControls");
963    });
964    $(".sectionPricelists button").on("click", function () {
965        let elm = $(this);
966
967        let spanContent = $(elm).find("span").html();
968        $(elm).css({
969            width: $(elm).outerWidth() + "px",
970            height: $(elm).outerHeight() + "px",
971        });
972        $(elm).find("span").html("");
973        $(elm).addClass("loading");
974        $("body").addClass("loading");
975        fetch($(elm).attr("content"))
976            .then((resp) => resp.blob())
977            .then((blob) => {
978                const url = window.URL.createObjectURL(blob);
979                const a = document.createElement("a");
980                a.style.display = "none";
981                a.href = url;
982                let mDate = new Date(Date.now());
983                a.download = $(elm).data("filename");
984                document.body.appendChild(a);
985                let classToAdd = "success";
986                blob.size ? a.click() : (classToAdd = "error");
987                a.remove();
988                window.URL.revokeObjectURL(url);
989
990                $(elm).removeClass("loading").addClass(classToAdd).attr("last-class", classToAdd);
991                $("body").removeClass("loading");
992                setTimeout(function () {
993                    $(elm).removeClass(classToAdd).find("span").html(spanContent);
994                    $(elm).css({ width: "", height: "" });
995                }, 2000);
996            })
997            .catch(() => {});
998    });
999    $("#search_input")
1000        .on("keydown", function (e) {
1001            if (e.keyCode === 13) {
1002                window.location.href = "/search?query=" + $(this).val();
1003            }
1004        })
1005        .on("focus", function () {
1006            this.setSelectionRange(0, this.value.length);
1007        });
1008}
1009function variablesInit() {
1010    let bg =
1011        "url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='" +
1012        onSurface.replace("#", "%23") +
1013        "' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2.5' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e\")";
1014    $(".navbar-toggler-icon").css("background-image", bg);
1015    cookie_consent = getCookie("user_cookie_consent");
1016    mobile = $(".mobileTrue").length > 0;
1017    footerHeight = $("footer").outerHeight();
1018    $(":root").css("--_footerHeight", footerHeight + "px");
1019    const lang = getCookie("_scLanguage") || $("#lang-dropdown div").attr("data-lang") || "en-US";
1020    $("html").attr("lang", lang.split("-")[0]);
1021}
1022
1023// --- CATEGORY & PRODUCT --- //
1024function updateProduct(elm, basketGuid, lineGuid) {
1025    clearTimeout(clearTimeout_updateProduct);
1026
1027    clearTimeout_updateProduct = setTimeout(function () {
1028        elm.removeClass("scError");
1029
1030        let qty = elm.val();
1031
1032        $("body").addClass("loading");
1033
1034        $.ajax({
1035            method: "POST",
1036            url: "/api/basket/edit/" + basketGuid + "/lines/" + lineGuid + "/quantity/" + qty,
1037            cache: false,
1038        })
1039            .done(function (data) {
1040                parseBasket(data);
1041            })
1042            .fail(function (xhr, status, error) {
1043                alert(xhr.responseText);
1044                document.location.href = document.location.href.split("#")[0];
1045            })
1046            .always(function () {
1047                $("body").removeClass("loading");
1048            });
1049    }, 600);
1050}
1051function addProduct(e, elm, product, uom, variantcode, showlotdialog) {
1052    if ($(elm).closest(".interactDisabled").length) {
1053        return;
1054    }
1055    if (window.self !== window.top) return;
1056    e.preventDefault();
1057    let qty;
1058
1059    for (let i = 0; i < 10; i++) {
1060        if (elm.parents().eq(i).find(".ProductAmountInput").length == 1) {
1061            qty = elm.parents().eq(i).find(".ProductAmountInput").val();
1062            break;
1063        }
1064    }
1065
1066    if (showlotdialog) {
1067        createLotDialog(elm, product, uom, variantcode, qty);
1068        return;
1069    }
1070
1071    $("body").addClass("loading");
1072
1073    //For the product card
1074    let itemVariantId;
1075    for (let i = 1; i < 10; i++) {
1076        if (elm.parents().eq(i).find(".itemVariantId option:selected").length == 1) {
1077            itemVariantId = elm.parents().eq(i).find(".itemVariantId option:selected").val();
1078            break;
1079        }
1080    }
1081
1082    //For the Favorites page
1083    if (variantcode != "" && variantcode != null && variantcode != undefined) {
1084        itemVariantId = variantcode;
1085    }
1086
1087    //To make sure we do not get variant code from product card when adding related items we look for the closets relatedProductsTable because if we find one, then we know we are in the related items table
1088    if (elm.closest(".relatedProductsTable").length > 0) {
1089        itemVariantId = variantcode;
1090    }
1091
1092    qty = qty || 1;
1093
1094    if (uom != null && uom != "") {
1095        uom = "/uom/" + uom;
1096    }
1097
1098    if (itemVariantId != null && itemVariantId != undefined && itemVariantId != "") {
1099        itemVariantId = "/itemvariantid/" + itemVariantId;
1100    } else {
1101        itemVariantId = "";
1102    }
1103
1104    let spanContent = $(elm).find("span").html();
1105    $(elm).css({
1106        width: $(elm).outerWidth() + "px",
1107        height: $(elm).outerHeight() + "px",
1108    });
1109    $(elm).find("span").html("");
1110    $(elm).addClass("loading");
1111
1112    let classToAdd = "success";
1113
1114    if (product.indexOf("/") > 0) {
1115        product = product.replace("/", "%2F");
1116    }
1117
1118    $.ajax({
1119        method: "POST",
1120        url: "/api/basket/add/" + encodeURIComponent(product) + "/quantity/" + qty + uom + itemVariantId,
1121        cache: false,
1122        dataType: "json",
1123        contentType: "application/json",
1124    })
1125        .done(function (data) {
1126            parseBasket(data);
1127            onProductAdded();
1128        })
1129        .fail(function (data, jqXHR, textStatus) {
1130            let response = data.responseText;
1131            let errorOverlay = $("<div>").addClass("overlay errorOverlay").html(response);
1132            $(elm).after(errorOverlay);
1133            $(errorOverlay)
1134                .find(":after")
1135                .on("click", function () {
1136                    $(errorOverlay).remove();
1137                });
1138            classToAdd = "error";
1139        })
1140        .always(function () {
1141            $(elm).removeClass("loading").addClass(classToAdd);
1142            $(elm).attr("last-class", classToAdd);
1143            setTimeout(function () {
1144                $(elm).removeClass(classToAdd);
1145                if (spanContent) $(elm).find("span").html(spanContent);
1146                $(elm).css({ width: "", height: "" });
1147                setTimeout(function () {
1148                    $(elm).blur();
1149                }, 350);
1150            }, 3000);
1151            $("body").removeClass("loading");
1152        });
1153}
1154function replacer(name, val) {
1155    // convert Number to string
1156    if (val && val.constructor === Number) {
1157        return val.toString();
1158    } else {
1159        return val; // return as is
1160    }
1161}
1162function addProductLotsToBasket(elm, product, uom, variantcode) {
1163    if ($(elm).closest(".interactDisabled").length) {
1164        return;
1165    }
1166    if (window.self !== window.top) return;
1167    let qty = 0;
1168    let data = [];
1169    $(".lotContainer input").each(function () {
1170        if (this.value != "" && this.value != undefined && this.value > 0) {
1171            data.push({
1172                ItemNo: product,
1173                Uom: uom,
1174                VariantCode: variantcode,
1175                Amount: this.value,
1176                LotNo: $(this).data("lot").toString(),
1177                LotInventoryLocationCode: $(this).data("inv"),
1178            });
1179            qty += parseFloat(this.value);
1180        }
1181    });
1182
1183    //Anything to add?
1184    if (qty > 0) {
1185        $("body").addClass("loading");
1186        $(elm).addClass("loading");
1187
1188        $.ajax({
1189            method: "POST",
1190            url: "/api/basket/addmanylot",
1191            cache: false,
1192            dataType: "json",
1193            contentType: "application/json",
1194            data: JSON.stringify(data, replacer),
1195        })
1196            .done(function (data) {
1197                $(elm).addClass("success").attr("last-class", "success");
1198                parseBasket(data);
1199                setTimeout(function () {
1200                    $(".lotDropdown").addClass("hiding");
1201                    setTimeout(function () {
1202                        $(".lotDropdown").remove();
1203                    }, 700);
1204                }, 700);
1205                onProductAdded();
1206            })
1207            .fail(function (jqXHR, response) {
1208                $(elm).addClass("error").attr("last-class", "error");
1209            })
1210            .always(function () {
1211                $("body").removeClass("loading");
1212                $(elm).removeClass("loading");
1213                setTimeout(function () {
1214                    $(elm).removeClass("success error");
1215                }, 3000);
1216            });
1217    }
1218}
1219function addMulitpleProducts(elm, bUpdate) {
1220    if (window.self !== window.top) return;
1221    let qty = 0;
1222    elm.removeClass("scError");
1223    //elm.children.find('')
1224    let data = [];
1225    $("body").addClass("loading");
1226
1227    $(".ProductMatrixAmountInput").each(function (index) {
1228        let VariantId = $("input.ProductMatrixVariantId");
1229        let itemNo = $("input.ProductMatrixItemNo");
1230        let itemUOM = $("input.ProductMatrixUOM");
1231        let oldQty = $(this).parent().siblings("input.ProductMatrixOldQty").attr("value");
1232        if (this.value != oldQty) {
1233            if (VariantId == null || VariantId[index] == null) {
1234                data.push({
1235                    ItemNo: itemNo[index].value,
1236                    Uom: itemUOM[index].value,
1237                    Amount: this.value,
1238                });
1239            } else {
1240                data.push({
1241                    ItemNo: itemNo[index].value,
1242                    Uom: itemUOM[index].value,
1243                    Amount: this.value,
1244                    VariantId: VariantId[index].value,
1245                });
1246            }
1247            qty += parseFloat(this.value);
1248            $(this).addClass("valueChanged");
1249        }
1250    });
1251
1252    // new variant renderer
1253
1254    $(".variantItem").each(function () {
1255        const itemNo = $(this).data("itemno");
1256        const variantCode = $(this).data("variantid");
1257        const itemUOM = $(this).data("uom");
1258        const oldQty = $(this).data("oldqty");
1259        const newQty = $(this).find("input").val();
1260
1261        if (newQty == oldQty) return;
1262        $(this).data("oldqty", newQty);
1263        data.push({
1264            ItemNo: String(itemNo),
1265            VariantId: String(variantCode),
1266            Uom: itemUOM,
1267            Amount: newQty,
1268        });
1269        $(this).addClass("valueChanged");
1270    });
1271
1272    strUrl = "/api/basket/addmany";
1273
1274    if (bUpdate) {
1275        strUrl = "/api/basket/updatemany";
1276    }
1277
1278    let spanContent = $(elm).find("span").html();
1279    $(elm).css({
1280        width: $(elm).outerWidth() + "px",
1281        height: $(elm).outerHeight() + "px",
1282    });
1283    $(elm).find("span").html("");
1284    $(elm).addClass("loading");
1285
1286    let classToAdd = "upToDate";
1287
1288    if (!$(".valueChanged").length) {
1289        $(elm).removeClass("loading").addClass(classToAdd);
1290        $(elm).attr("last-class", classToAdd);
1291        setTimeout(function () {
1292            $(elm).removeClass(classToAdd);
1293            if (spanContent) $(elm).find("span").html(spanContent);
1294            setTimeout(function () {
1295                $(elm).blur();
1296            }, 350);
1297        }, 3000);
1298        $("body").removeClass("loading");
1299        return;
1300    }
1301
1302    classToAdd = "updated";
1303    $.ajax({
1304        method: "POST",
1305        url: strUrl,
1306        cache: false,
1307        dataType: "json",
1308        contentType: "application/json",
1309        data: JSON.stringify(data),
1310    })
1311        .done(function (data) {
1312            $(".valueChanged").each(function () {
1313                $(this).removeClass("valueChanged");
1314                $(this).parent().siblings("input.ProductMatrixOldQty").attr("value", this.value);
1315            });
1316            parseBasket(data);
1317            onProductAdded();
1318        })
1319        .fail(function (jqXHR, response) {
1320            msg = response;
1321            if (jqXHR.responseJSON) {
1322                msg = jqXHR.responseJSON.message + " (" + jqXHR.responseJSON.errorcode + ")";
1323                elm.find(".addedToCartText").text(msg);
1324            } else if (jqXHR.responseText) {
1325                msg = jqXHR.responseText;
1326                elm.find(".addedToCartText").text(msg);
1327            } else {
1328                msg = "An unknonw error ocurred, please contact the shop administrator.";
1329                elm.find(".addedToCartText").text(msg);
1330            }
1331            let errorOverlay = $("<div>").addClass("overlay errorOverlay").html(msg);
1332            $(elm).after(errorOverlay);
1333            $(errorOverlay)
1334                .find(":after")
1335                .on("click", function () {
1336                    $(errorOverlay).remove();
1337                });
1338            classToAdd = "error";
1339        })
1340        .always(function () {
1341            $(elm).removeClass("loading").addClass(classToAdd);
1342            $(elm).attr("last-class", classToAdd);
1343            setTimeout(function () {
1344                $(elm).removeClass(classToAdd);
1345                if (spanContent) $(elm).find("span").html(spanContent);
1346            }, 3000);
1347            $("body").removeClass("loading");
1348        });
1349}
1350function disableRemoveButtons() {
1351    if (window.self !== window.top) return;
1352    removeBtns.forEach((removeBtn) => {
1353        removeBtn.style.pointerEvents = "none";
1354
1355        setTimeout(() => {
1356            removeBtn.style.pointerEvents = "all";
1357        }, 5000);
1358    });
1359}
1360function addVariantListToCart(elm) {
1361    if (window.self !== window.top) return;
1362    let qty = 0;
1363    let data = [];
1364    $(".line").each(function () {
1365        let itemNo = $(this).data("sku").toString();
1366        let itemUOM = $(this).data("uomcode");
1367        let itemVariantId = $(this).data("varid");
1368        let itemQty = $(this).find(".incDecInput").val();
1369        qty += parseFloat(itemQty);
1370        data.push({
1371            ItemNo: itemNo,
1372            Uom: itemUOM,
1373            Amount: itemQty,
1374            VariantId: itemVariantId,
1375        });
1376    });
1377
1378    $("body").addClass("loading");
1379    strUrl = "/api/basket/addmany/force";
1380
1381    let spanContent = $(elm).find("span").html();
1382    $(elm).css("width", $(elm).outerWidth() + "px");
1383    $(elm).css("height", $(elm).outerHeight() + "px");
1384    $(elm).find("span").html("");
1385    $(elm).addClass("loading");
1386
1387    $.ajax({
1388        method: "POST",
1389        url: strUrl,
1390        cache: false,
1391        dataType: "json",
1392        contentType: "application/json",
1393        data: JSON.stringify(data),
1394    })
1395        .done(function (data) {
1396            $(elm).addClass("success").attr("last-class", "success");
1397            parseBasket(data);
1398            onProductAdded();
1399        })
1400        .fail(function (jqXHR, textStatus) {
1401            $(elm).addClass("error").attr("last-class", "error");
1402            //todo error handling
1403        })
1404        .always(function () {
1405            $("body").removeClass("loading");
1406            $(elm).removeClass("loading");
1407            setTimeout(function () {
1408                $(elm).removeClass("success error");
1409            }, 3000);
1410            if (spanContent) $(elm).find("span").html(spanContent);
1411            $(elm).css({ width: "", height: "" });
1412        });
1413}
1414function updateProductDiscount(elm, basketGuid, lineGuid) {
1415    if (window.self !== window.top) return;
1416    clearTimeout(clearTimeout_updateProduct);
1417
1418    clearTimeout_updateProduct = setTimeout(function () {
1419        elm.removeClass("scError");
1420
1421        var discount = elm.val().replaceAll(",", ".");
1422
1423        let doubleRegex = /^(100(\.0{1,2})?|[0-9]{1,2}(\.[0-9]{1,4})?)$/;
1424        if (!doubleRegex.test(discount)) {
1425            alert("Discount must be a number between 0 and 100 with up to 4 decimal points");
1426            return false;
1427        }
1428
1429        let parsedDiscount = parseFloat(discount);
1430
1431        if (isNaN(parsedDiscount)) {
1432            alert("An error occured in the conversion to a double");
1433            return false;
1434        }
1435
1436        $("body").addClass("loading");
1437
1438        $.ajax({
1439            method: "POST",
1440            url: "/api/basket/edit/" + basketGuid + "/lines/" + lineGuid + "/discount/" + parsedDiscount,
1441            cache: false,
1442        })
1443            .done(function (data) {
1444                parseBasket(data);
1445            })
1446            .fail(function (xhr, status, error) {
1447                alert(xhr.responseText);
1448
1449                document.location.href = document.location.href.split("#")[0];
1450            })
1451            .always(function () {
1452                $("body").removeClass("loading");
1453            });
1454    }, 600);
1455}
1456function createLotDialog(elm, product, uom, variantcode, qty) {
1457    $.ajax({
1458        method: "GET",
1459        url:
1460            "/shop/api/productlothtml?productno=" +
1461            encodeURIComponent(product) +
1462            "&variantcode=" +
1463            encodeURIComponent(variantcode) +
1464            "&quantity=" +
1465            encodeURIComponent(qty) +
1466            "&uom=" +
1467            encodeURIComponent(uom),
1468        cache: false,
1469    }).done(function (data) {
1470        let totalQty = parseInt(qty);
1471        $(elm).parent().addClass("lotSection");
1472        let lotDropdown = $("<div class='dropdown lotDropdown'>");
1473        let lotContainer = $("<div>").addClass("lotContainer");
1474        let closeButton = $("<i>")
1475            .addClass("fas fa-times closeButton")
1476            .on("click", function () {
1477                const dropdown = $(this).parent();
1478                dropdown.addClass("hiding");
1479                setTimeout(function () {
1480                    const lotSection = dropdown.parent();
1481                    dropdown.remove();
1482                    lotSection.removeClass("lotSection");
1483                }, 500);
1484            });
1485        $(data).each(function () {
1486            let lotNo = this.lotNo;
1487            let invNo = this.locationNo;
1488            let quantity = this.quantity;
1489            let currentQty = 0;
1490
1491            if (totalQty > quantity) {
1492                currentQty = quantity;
1493                totalQty -= quantity;
1494            } else {
1495                currentQty = totalQty < 0 ? 0 : totalQty;
1496                totalQty = 0;
1497            }
1498
1499            let title = $("<span>").html(lotNo);
1500            let input = $("<input>")
1501                .attr({
1502                    type: "text",
1503                    min: 0,
1504                    max: quantity,
1505                    "data-inv": invNo,
1506                    "data-lot": lotNo,
1507                    value: currentQty,
1508                })
1509                .val(currentQty);
1510            let inventory = $("<span>")
1511                .addClass("lotInv")
1512                .html("/" + quantity);
1513            lotContainer.append(title, input, inventory);
1514        });
1515        let title = $("<span>").html("No LOT");
1516        let input = $("<input>").attr({ type: "number", min: 0, "data-lot": "", value: totalQty }).val(totalQty);
1517        let inventory = $("<span>").addClass("lotInv").html("/∞");
1518        if (AddToCartIfOutOfStock) {
1519            lotContainer.append(title, input, inventory);
1520        }
1521
1522        const addButton = $("<button>")
1523            .addClass("btn btn-sm btn-success addLot addToCartAni")
1524            .append($("<span>").html(elm.find("span").html()));
1525        addButton.on("click", function () {
1526            addProductLotsToBasket(this, product, uom, variantcode);
1527        });
1528        lotDropdown.append(closeButton, lotContainer, addButton);
1529        $(elm).after(lotDropdown);
1530        $(lotDropdown).css("max-height", $(lotDropdown).outerHeight());
1531
1532        //This is used to make sure you do not enter more than max-value in the lot numbers
1533        $(".lotContainer input").blur(function () {
1534            if (parseInt($(this).val()) > parseInt($(this).attr("max"))) {
1535                $(this).val($(this).attr("max"));
1536            } else if (parseInt($(this).val()) < parseInt($(this).attr("min"))) {
1537                $(this).val($(this).attr("min"));
1538            }
1539        });
1540    });
1541}
1542function sideControlsToggle() {
1543    if ($(".showSideControls").length) $(".showSideControls").removeClass("showSideControls");
1544    else $("body").addClass("showSideControls");
1545}
1546function selectVariant(event) {
1547    let elm;
1548
1549    if (event.type === "click") {
1550        elm = $(event.currentTarget);
1551    } else if (event.type === "change" && $(event.target).is("select")) {
1552        // For select change events, get the selected option which has the data attributes
1553        elm = $(event.target).find("option:selected");
1554    } else {
1555        elm = $(event.target);
1556    }
1557    if (elm.hasClass("selected")) return;
1558    $("body").addClass("loading");
1559    $(".variantItem.selected").removeClass("selected");
1560    $(elm).addClass("selected").siblings().removeClass("selected");
1561
1562    //Set the new image
1563    const image = $(elm).data("image");
1564    if (image) {
1565        $("#mainimage").attr("src", image);
1566    } else if (typeof VariantStayOnNoImage === "undefined" || !VariantStayOnNoImage) {
1567        $("#mainimage").attr("src", $("#mainimage").data("original-src"));
1568    }
1569
1570    //Load data
1571    let url = decodeURIComponent(elm.data("newloc"));
1572
1573    if (!url || url === "undefined") {
1574        $("body").removeClass("loading");
1575        return;
1576    }
1577
1578    $("<div>").load(url, function (newVariant) {
1579        if (event.type === "change") {
1580            //replace the interact section
1581            $(".ProductInteract").replaceWith($(newVariant).find(".ProductInteract"));
1582            incDecInit();
1583            AddToCartBtn();
1584            favoritesButtonInit();
1585        }
1586
1587        //Replace the information section
1588        $(".ProductInfo").replaceWith($(newVariant).find(".ProductInfo"));
1589
1590        //Replace Product Name
1591        $(".ProductNameHeader .header").html($(newVariant).find(".ProductNameHeader").html());
1592        $("body").removeClass("loading");
1593        $(".InventoryLocation").addClass(inventoryLocDisplay);
1594    });
1595}
1596
1597// Filtering & Sorting
1598function setUrlParams() {
1599    //empty params
1600    let query = searchParams.get("query");
1601    window.history.replaceState({}, "", window.location.href.split("?")[0]);
1602    if (query) pushUrlParam("query", query);
1603
1604    //filtering
1605    $(".sectionFilter")
1606        .find(".filterSection:has(input:checked)")
1607        .each(function () {
1608            let section = this;
1609            let selected = "";
1610            $(section)
1611                .find("input:checked")
1612                .each(function () {
1613                    if (selected != "") selected += URIJoiner;
1614                    selected += $(this).attr("id").split(URIEquals)[1];
1615                });
1616            let sectionName = $(this).find("input").first().attr("id").split(URIEquals)[0];
1617            pushUrlParam("filter", sectionName + URIEquals + selected);
1618        });
1619    //range filtering (numeric value sliders)
1620    $(".sectionFilter double-slider").each(function () {
1621        if (this.isDefault) return;
1622        pushUrlParam("range", $(this).attr("data-code") + URIEquals + this.valueMin + ".." + this.valueMax);
1623    });
1624    //sorting
1625    let newSorting = $("#sortDropdown").val();
1626    if (newSorting == defaultSortingMethod) pushUrlParam("sort", null);
1627    else pushUrlParam("sort", newSorting);
1628    //view
1629    let newGalleryClass = $(".btn-layout:has(:checked) input").attr("value");
1630    if (newGalleryClass == defaultGalleryClass) pushUrlParam("view", null);
1631    else pushUrlParam("view", newGalleryClass);
1632    checkUrlParams();
1633}
1634function checkUrlParams() {
1635    searchParams = new URLSearchParams(window.location.search);
1636    let filters = searchParams.getAll("filter");
1637    let ranges = searchParams.getAll("range");
1638    if (filters) {
1639        $(filters).each(function () {
1640            let type = this.split(URIEquals)[0];
1641            let content = this.split(URIEquals)[1].split(URIJoiner);
1642            $(content).each(function () {
1643                let newId = type + URIEquals + this;
1644                $(".filterSpecifications")
1645                    .filter(function () {
1646                        const currentId = decodeURIComponent($(this).attr("id"));
1647                        return currentId == newId;
1648                    })
1649                    .attr("checked", "checked");
1650            });
1651        });
1652    }
1653    if (ranges) {
1654        $(ranges).each(function () {
1655            let code = this.split(URIEquals)[0];
1656            let rangeStr = this.split(URIEquals)[1];
1657            if (!rangeStr || rangeStr.indexOf("..") === -1) return;
1658            let parts = rangeStr.split("..");
1659            let minVal = parts[0];
1660            let maxVal = parts[1];
1661            let target = $("double-slider").filter(function () {
1662                return $(this).attr("data-code") == code;
1663            });
1664            if (minVal !== "") target.attr("value-min", minVal);
1665            if (maxVal !== "") target.attr("value-max", maxVal);
1666        });
1667    }
1668    productsFilter();
1669    let sort = searchParams.get("sort");
1670    if (sort) {
1671        sortingMethod = sort;
1672        $("#sortDropdown").val(sortingMethod);
1673    } else {
1674        sortingMethod = defaultSortingMethod;
1675    }
1676    productsSort();
1677    let view = searchParams.get("view");
1678    if (view) currentGalleryClass = view;
1679    galleryViewInit();
1680    CategoryPagerV2.refreshIfChanged();
1681}
1682function pushUrlParam(param, val) {
1683    var url = window.location.href;
1684    var newParam = param + "=" + val;
1685    if (url.indexOf("?") !== -1) {
1686        newParam = "&" + newParam;
1687    } else {
1688        newParam = "?" + newParam;
1689    }
1690    if (val == null) {
1691        newParam = "";
1692    }
1693    url += newParam;
1694    window.history.replaceState({}, "", url);
1695}
1696function productsFilter() {
1697    if (document.getElementById("categoryIndexV2")) return;
1698    searchParams = new URLSearchParams(window.location.search);
1699    let filters = searchParams.getAll("filter");
1700    let ranges = searchParams.getAll("range");
1701    let productItems = $(".ProductItem");
1702    $(".ProductItem.hide").removeClass("hide");
1703    productItems.each(function () {
1704        for (let i = 0; i < filters.length; i++) {
1705            let filterBy = filters[i].split(URIEquals)[0].toLowerCase().replaceAll(" ", "-");
1706            let specContent = decodeURIComponent($(this).attr("spec-" + filterBy));
1707            if (!specContent) {
1708                $(this).addClass("hide");
1709                continue;
1710            }
1711            specContent = specContent.split(URISplitter);
1712            let filterContent = filters[i].split(URIEquals)[1].split(URIJoiner);
1713            if (!filterContent.some((item) => specContent.includes(item))) $(this).addClass("hide");
1714        }
1715        for (let j = 0; j < ranges.length; j++) {
1716            let rangeBy = ranges[j].split(URIEquals)[0].toLowerCase().replaceAll(" ", "-");
1717            let rangeStr = ranges[j].split(URIEquals)[1];
1718            if (!rangeStr || rangeStr.indexOf("..") === -1) continue;
1719            let parts = rangeStr.split("..");
1720            let minVal = parts[0] === "" ? Number.NEGATIVE_INFINITY : parseFloat(parts[0]);
1721            let maxVal = parts[1] === "" ? Number.POSITIVE_INFINITY : parseFloat(parts[1]);
1722            if (isNaN(minVal)) minVal = Number.NEGATIVE_INFINITY;
1723            if (isNaN(maxVal)) maxVal = Number.POSITIVE_INFINITY;
1724            let specnumAttr = $(this).attr("specnum-" + rangeBy);
1725            if (!specnumAttr) {
1726                $(this).addClass("hide");
1727                continue;
1728            }
1729            let nums = specnumAttr
1730                .split(URISplitter)
1731                .map(parseFloat)
1732                .filter((n) => !isNaN(n));
1733            if (!nums.some((n) => n >= minVal && n <= maxVal)) $(this).addClass("hide");
1734        }
1735    });
1736    if ($(".TotalProducts").length) {
1737        let totalProductsTitle = $(".TotalProducts").html().split(" ")[1];
1738        let filteredProductsCount = $(".ProductItem:not(.hide)").length;
1739        //$(".TotalProducts").html(filteredProductsCount + " " + totalProductsTitle);
1740    }
1741    refreshProductGroupCounts();
1742    FilterFacets.refresh(FilterFacets.domEntries());
1743}
1744function refreshProductGroupCounts() {
1745    $(".productGroup").each(function () {
1746        const visibleCount = $(this).find(".ProductItem:not(.hide)").length;
1747        $(this).find(".productGroupTotal").text(visibleCount);
1748        $(this).prop("hidden", visibleCount === 0);
1749    });
1750}
1751function productsSort() {
1752    if (document.getElementById("categoryIndexV2")) return;
1753    let activeProducts = $(".ProductItem:not(.hide)");
1754    $(activeProducts).css("order", "0");
1755    activeProducts.sort(compareAttributes);
1756    for (let i = 0; i < activeProducts.length; i++) {
1757        $(activeProducts[i]).css("order", i);
1758    }
1759}
1760function compareAttributes(a, b) {
1761    let method = "name";
1762    let direction = 1;
1763    switch (sortingMethod) {
1764        case "name-za":
1765            direction = -1;
1766            break;
1767        case "price-09":
1768            method = "price";
1769            break;
1770        case "price-90":
1771            method = "price";
1772            direction = -1;
1773            break;
1774        case "sku-az":
1775            method = "sku";
1776            break;
1777        case "sku-za":
1778            method = "sku";
1779            direction = -1;
1780            break;
1781        case "relevance":
1782            method = "relevance";
1783            break;
1784        default:
1785            break;
1786    }
1787
1788    let aVal = $(a)
1789        .attr("data-" + method)
1790        .toLowerCase();
1791    let bVal = $(b)
1792        .attr("data-" + method)
1793        .toLowerCase();
1794    if (method == "price") {
1795        aVal = parseFloat(aVal);
1796        bVal = parseFloat(bVal);
1797        if (aVal == "") aVal = direction > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
1798        if (bVal == "") bVal = direction > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
1799    }
1800
1801    if (aVal < bVal) return -1 * direction;
1802    else if (bVal < aVal) return direction;
1803    else return 0;
1804}
1805function collapseFilter(btn) {
1806    $(btn).toggleClass("show");
1807}
1808// Initializazions
1809function filterInit() {
1810    if (sideFilter) {
1811        if (!$(".galleryControls").length) return;
1812        $("body").addClass("sideControls");
1813        $($(".galleryControls")[1]).addClass("d-none");
1814    }
1815    $(".sectionFilter").on("change", "input, double-slider", function (e) {
1816        setUrlParams();
1817        // Check if we need to load more products after filtering
1818        endlessScrollCheckVisibleProducts();
1819        e.stopPropagation();
1820    });
1821    $(".sectionFilter").on("click", ".sliderReset", function (e) {
1822        let slider = $(this).closest(".filterRange").find("double-slider")[0];
1823        if (!slider || slider.isDefault) return;
1824        slider.reset();
1825        setUrlParams();
1826        // Check if we need to load more products after clearing the range
1827        endlessScrollCheckVisibleProducts();
1828    });
1829    $(".sectionFilter .clearFilters").on("click", function (e) {
1830        $(".sectionFilter input:checked").prop("checked", false);
1831        $(".sectionFilter double-slider").each(function () {
1832            this.reset();
1833        });
1834        setUrlParams();
1835        // Check if we need to load more products after clearing filters
1836        endlessScrollCheckVisibleProducts();
1837    });
1838}
1839
1840// Endless Scroll for Subcategory Products
1841function endlessScrollInit() {
1842    let container = $("#subCatContainer");
1843    if (!container.length) return;
1844
1845    // Check if pagination is enabled
1846    let paginationEnabled = container.data("pagination-enabled") === true || container.data("pagination-enabled") === "true";
1847    if (!paginationEnabled) return;
1848
1849    // Read initial state from data attributes
1850    endlessScrollCategoryNo = container.data("category");
1851    endlessScrollCurrentPage = parseInt(container.data("page")) || 1;
1852    endlessScrollPageSize = parseInt(container.data("pagesize")) || 48;
1853    endlessScrollTotalCount = parseInt(container.data("total")) || 0;
1854    endlessScrollHasMore = container.data("hasmore") === true || container.data("hasmore") === "true";
1855
1856    // Set up scroll event listener
1857    $(window).on("scroll.endlessScroll", endlessScrollOnScroll);
1858
1859    // Initial check - in case the page is short and we need to load more
1860    setTimeout(function () {
1861        endlessScrollCheckVisibleProducts();
1862    }, 500);
1863}
1864
1865function endlessScrollOnScroll() {
1866    if (endlessScrollLoading || !endlessScrollHasMore) return;
1867
1868    let loader = $("#subProdsLoader");
1869    if (!loader.length) return;
1870
1871    // Check if the loader is visible in viewport (with some buffer for early loading)
1872    let loaderTop = loader.offset().top;
1873    let windowBottom = $(window).scrollTop() + $(window).height();
1874    let buffer = 300; // Load 300px before the loader is visible
1875
1876    if (windowBottom + buffer >= loaderTop) {
1877        endlessScrollLoadMore();
1878    }
1879}
1880
1881function endlessScrollLoadMore() {
1882    if (endlessScrollLoading || !endlessScrollHasMore) return;
1883
1884    endlessScrollLoading = true;
1885    let loader = $("#subProdsLoader");
1886    loader.addClass("loading");
1887
1888    let nextPage = endlessScrollCurrentPage + 1;
1889
1890    $.ajax({
1891        method: "GET",
1892        url: "/api/category/" + encodeURIComponent(endlessScrollCategoryNo) + "/subproducts",
1893        data: {
1894            page: nextPage,
1895            pageSize: endlessScrollPageSize,
1896        },
1897        cache: false,
1898        dataType: "json",
1899    })
1900        .done(function (response) {
1901            if (response.html && response.html.length > 0) {
1902                // Append new products to the list
1903                let productList = $("#subProductList");
1904                productList.append(response.html);
1905
1906                // Update state
1907                endlessScrollCurrentPage = response.currentPage;
1908                endlessScrollHasMore = response.hasMore;
1909
1910                // Update container data attributes
1911                $("#subCatContainer").data("page", endlessScrollCurrentPage);
1912                $("#subCatContainer").data("hasmore", endlessScrollHasMore);
1913
1914                // Initialize lazy loading for new images
1915                if (typeof lazyload === "function") {
1916                    lazyload();
1917                }
1918
1919                // Re-initialize add to cart buttons for new products
1920                AddToCartBtn();
1921
1922                // Re-initialize increment/decrement inputs for new products
1923                incDecInit();
1924
1925                // Apply current sorting to all products including new ones
1926                productsSort();
1927
1928                // Apply current filters to new products
1929                productsFilter();
1930
1931                // Re-initialize favorites buttons
1932                if (typeof favoritesButtonInit === "function") {
1933                    favoritesButtonInit();
1934                }
1935            }
1936
1937            // Hide loader if no more products
1938            if (!endlessScrollHasMore) {
1939                loader.removeClass("loading").hide();
1940            }
1941
1942            // Check if we need to load more (filtering might have hidden the new products)
1943            setTimeout(function () {
1944                endlessScrollCheckVisibleProducts();
1945            }, 100);
1946        })
1947        .fail(function (xhr, status, error) {
1948            console.error("Failed to load more products:", error);
1949            // Show error state on loader
1950            loader.removeClass("loading").addClass("error");
1951            setTimeout(function () {
1952                loader.removeClass("error");
1953            }, 3000);
1954        })
1955        .always(function () {
1956            endlessScrollLoading = false;
1957            loader.removeClass("loading");
1958        });
1959}
1960
1961function endlessScrollCheckVisibleProducts() {
1962    if (!endlessScrollHasMore || endlessScrollLoading) return;
1963    if (!$("#subProductList").length) return;
1964
1965    // Count visible products in subcategory list (not hidden by filters)
1966    let visibleCount = $("#subProductList .ProductItem:not(.hide)").length;
1967
1968    // If we have fewer visible products than the minimum threshold, load more
1969    if (visibleCount < endlessScrollMinVisibleProducts) {
1970        endlessScrollLoadMore();
1971    }
1972}
1973
1974const FilterFacets = {
1975    refresh(entries) {
1976        const inputs = $(".sectionFilter .filterSpecifications");
1977        if (!inputs.length) return;
1978
1979        const counts = this.countOptions(entries, this.activeFilters());
1980        inputs.each((i, input) => {
1981            const option = this.parseOption(input.id);
1982            const count = counts.get(this.optionKey(option.code, option.value)) || 0;
1983            input.disabled = count === 0 && !input.checked;
1984            $(input).siblings(".filterCount").text(count);
1985        });
1986    },
1987
1988    codeKey(code) {
1989        return code.toLowerCase().replaceAll(" ", "-");
1990    },
1991
1992    optionKey(code, value) {
1993        return code + URIEquals + value;
1994    },
1995
1996    parseOption(id) {
1997        const split = id.indexOf(URIEquals);
1998        return {
1999            code: this.codeKey(id.substring(0, split)),
2000            value: decodeURIComponent(id.substring(split + URIEquals.length)),
2001        };
2002    },
2003
2004    activeFilters() {
2005        const params = new URLSearchParams(window.location.search);
2006        const filters = params.getAll("filter").map((raw) => ({
2007            code: this.codeKey(raw.split(URIEquals)[0]),
2008            wanted: raw.split(URIEquals)[1].split(URIJoiner),
2009        }));
2010
2011        const ranges = [];
2012        params.getAll("range").forEach((raw) => {
2013            const bounds = raw.split(URIEquals)[1];
2014            if (!bounds || bounds.indexOf("..") === -1) return;
2015            const parts = bounds.split("..");
2016            const min = parseFloat(parts[0]);
2017            const max = parseFloat(parts[1]);
2018            ranges.push({
2019                code: this.codeKey(raw.split(URIEquals)[0]),
2020                min: isNaN(min) ? Number.NEGATIVE_INFINITY : min,
2021                max: isNaN(max) ? Number.POSITIVE_INFINITY : max,
2022            });
2023        });
2024
2025        return { filters, ranges };
2026    },
2027
2028    domEntries() {
2029        return $(".ProductItem")
2030            .map((i, node) => {
2031                const specs = {};
2032                const nums = {};
2033                for (const attr of node.attributes) {
2034                    if (attr.name.startsWith("specnum-")) {
2035                        nums[attr.name.substring(8)] = attr.value
2036                            .split(URISplitter)
2037                            .map(parseFloat)
2038                            .filter((n) => !isNaN(n));
2039                    } else if (attr.name.startsWith("spec-")) {
2040                        specs[attr.name.substring(5)] = decodeURIComponent(attr.value).split(URISplitter);
2041                    }
2042                }
2043                return { specs, nums };
2044            })
2045            .get();
2046    },
2047
2048    passesFilter(entry, filter) {
2049        const own = entry.specs[filter.code];
2050        return !!own && filter.wanted.some((value) => own.includes(value));
2051    },
2052
2053    passesRanges(entry, ranges) {
2054        return ranges.every((range) => {
2055            const own = entry.nums[range.code];
2056            return !!own && own.some((value) => value >= range.min && value <= range.max);
2057        });
2058    },
2059
2060    countOptions(entries, active) {
2061        const counts = new Map();
2062        entries.forEach((entry) => {
2063            if (!this.passesRanges(entry, active.ranges)) return;
2064
2065            const failed = active.filters.filter((filter) => !this.passesFilter(entry, filter));
2066            if (failed.length > 1) return;
2067
2068            const codes = failed.length ? [failed[0].code] : Object.keys(entry.specs);
2069            codes.forEach((code) => {
2070                new Set(entry.specs[code] || []).forEach((value) => {
2071                    const key = this.optionKey(code, value);
2072                    counts.set(key, (counts.get(key) || 0) + 1);
2073                });
2074            });
2075        });
2076        return counts;
2077    },
2078};
2079
2080const CategoryPagerV2 = {
2081    active: false,
2082    loading: false,
2083    index: null,
2084    entries: [],
2085    facetEntries: [],
2086    matched: [],
2087    cards: new Map(),
2088    codeIndex: {},
2089    valueIndex: [],
2090    numCodeIndex: {},
2091    groupTemplate: null,
2092    groupByNo: {},
2093    groupCounts: {},
2094    pageSize: 48,
2095    offset: 0,
2096    rendered: 0,
2097    lastSignature: "",
2098
2099    init() {
2100        const source = document.getElementById("categoryIndexV2");
2101        if (!source) return;
2102
2103        this.index = JSON.parse(source.textContent);
2104        this.pageSize = this.index.pageSize || 48;
2105        this.groupTemplate = document.getElementById("productGroupsV2");
2106        this.buildLookups();
2107        this.adoptRenderedCards();
2108        this.active = true;
2109        this.lastSignature = this.signature();
2110        $(window).on("scroll.categoryPagerV2", () => this.onScroll());
2111
2112        if (this.hasListParams()) {
2113            this.refresh();
2114            return;
2115        }
2116
2117        this.matched = this.matchedNos();
2118        this.offset = (this.currentPageFromUrl() - 1) * this.pageSize;
2119        this.rendered = $("#productList .ProductItem").length;
2120        FilterFacets.refresh(this.facetEntries);
2121        this.updateTotal();
2122        this.updateState();
2123        this.onScroll();
2124    },
2125
2126    buildLookups() {
2127        this.index.codes.forEach((code, i) => (this.codeIndex[code] = i));
2128        this.index.numCodes.forEach((code, i) => (this.numCodeIndex[code] = i));
2129
2130        this.valueIndex = this.index.values.map((list) => {
2131            const lookup = {};
2132            list.forEach((value, i) => (lookup[value] = i));
2133            return lookup;
2134        });
2135
2136        this.entries = this.index.items.map((item) => ({
2137            no: item[0],
2138            specs: this.unpack(item[1], (text) => parseInt(text, 10)),
2139            nums: this.unpack(item[2], (text) => parseFloat(text)),
2140            ranks: item[3],
2141            group: item[4],
2142        }));
2143
2144        this.entries.forEach((entry) => (this.groupByNo[entry.no] = entry.group));
2145        this.facetEntries = this.entries.map((entry) => this.toFacetEntry(entry));
2146    },
2147
2148    toFacetEntry(entry) {
2149        const specs = {};
2150        const nums = {};
2151        Object.keys(entry.specs).forEach((codeSlot) => {
2152            specs[FilterFacets.codeKey(this.index.codes[codeSlot])] = entry.specs[codeSlot].map((slot) => this.index.values[codeSlot][slot]);
2153        });
2154        Object.keys(entry.nums).forEach((codeSlot) => {
2155            nums[FilterFacets.codeKey(this.index.numCodes[codeSlot])] = entry.nums[codeSlot];
2156        });
2157        return { specs, nums };
2158    },
2159
2160    unpack(packed, parse) {
2161        const groups = {};
2162        if (!packed) return groups;
2163        packed.split(";").forEach((group) => {
2164            const parts = group.split(":");
2165            groups[parseInt(parts[0], 10)] = parts[1].split(",").map(parse);
2166        });
2167        return groups;
2168    },
2169
2170    adoptRenderedCards() {
2171        $("#productList .ProductItem").each((i, node) => this.cards.set($(node).attr("data-sku"), $(node)));
2172    },
2173
2174    currentPageFromUrl() {
2175        const page = parseInt(new URLSearchParams(window.location.search).get("page"), 10);
2176        return page > 1 ? page : 1;
2177    },
2178
2179    hasListParams() {
2180        const params = new URLSearchParams(window.location.search);
2181        return params.getAll("filter").length > 0 || params.getAll("range").length > 0 || !!params.get("sort");
2182    },
2183
2184    signature() {
2185        const params = new URLSearchParams(window.location.search);
2186        return JSON.stringify([params.getAll("filter"), params.getAll("range"), params.get("sort")]);
2187    },
2188
2189    sortSlot() {
2190        const method = sortingMethod || defaultSortingMethod || "name-az";
2191        return {
2192            slot: this.index.sorts.indexOf(method.split("-")[0]),
2193            descending: method === "name-za" || method === "sku-za" || method === "price-90",
2194        };
2195    },
2196
2197    matchedNos() {
2198        const params = new URLSearchParams(window.location.search);
2199        const filters = [];
2200        let impossible = false;
2201
2202        params.getAll("filter").forEach((raw) => {
2203            const codeSlot = this.codeIndex[raw.split(URIEquals)[0]];
2204            if (codeSlot === undefined) {
2205                impossible = true;
2206                return;
2207            }
2208            filters.push({
2209                codeSlot: codeSlot,
2210                wanted: raw
2211                    .split(URIEquals)[1]
2212                    .split(URIJoiner)
2213                    .map((value) => this.valueIndex[codeSlot][value])
2214                    .filter((slot) => slot !== undefined),
2215            });
2216        });
2217
2218        const ranges = [];
2219
2220        params.getAll("range").forEach((raw) => {
2221            const bounds = raw.split(URIEquals)[1];
2222            if (!bounds || bounds.indexOf("..") === -1) return;
2223            const parts = bounds.split("..");
2224            ranges.push({
2225                codeSlot: this.numCodeIndex[raw.split(URIEquals)[0]],
2226                min: parts[0] === "" ? Number.NEGATIVE_INFINITY : parseFloat(parts[0]),
2227                max: parts[1] === "" ? Number.POSITIVE_INFINITY : parseFloat(parts[1]),
2228            });
2229        });
2230
2231        if (impossible) return [];
2232
2233        const matches = this.entries.filter((entry) => {
2234            for (const filter of filters) {
2235                const own = entry.specs[filter.codeSlot];
2236                if (!own || !filter.wanted.some((slot) => own.includes(slot))) return false;
2237            }
2238            for (const range of ranges) {
2239                const own = entry.nums[range.codeSlot];
2240                if (!own || !own.some((value) => value >= range.min && value <= range.max)) return false;
2241            }
2242            return true;
2243        });
2244
2245        const sort = this.sortSlot();
2246        if (sort.slot >= 0) {
2247            const direction = sort.descending ? -1 : 1;
2248            matches.sort((a, b) => this.compareGroups(a, b) || (a.ranks[sort.slot] - b.ranks[sort.slot]) * direction);
2249        }
2250
2251        return matches.map((entry) => entry.no);
2252    },
2253
2254    compareGroups(a, b) {
2255        return this.groupTemplate ? a.group - b.group : 0;
2256    },
2257
2258    countGroups() {
2259        this.groupCounts = {};
2260        this.matched.forEach((no) => {
2261            const group = this.groupByNo[no];
2262            this.groupCounts[group] = (this.groupCounts[group] || 0) + 1;
2263        });
2264    },
2265
2266    listFor(no) {
2267        const list = $("#productList");
2268        if (!this.groupTemplate) return list;
2269
2270        const group = this.groupByNo[no];
2271        let section = list.children(`.productGroup[data-group="${group}"]`);
2272        if (!section.length) {
2273            section = $(this.groupTemplate.content.querySelector(`.productGroup[data-group="${group}"]`).cloneNode(true));
2274            section.find(".productGroupTotal").text(this.groupCounts[group]);
2275            section.find(".GalleryBox").attr("class", "GalleryBox " + currentGalleryClass);
2276            list.append(section);
2277        }
2278        return section.children(".GalleryBox");
2279    },
2280
2281    refreshIfChanged() {
2282        if (!this.active) return;
2283        const signature = this.signature();
2284        if (signature === this.lastSignature) return;
2285        this.lastSignature = signature;
2286        this.refresh();
2287    },
2288
2289    refresh() {
2290        this.matched = this.matchedNos();
2291        this.offset = 0;
2292        this.rendered = 0;
2293        $("#productList").children().detach();
2294        FilterFacets.refresh(this.facetEntries);
2295        this.updateTotal();
2296        this.updateState();
2297        this.renderNext();
2298    },
2299
2300    renderNext() {
2301        if (this.loading) return;
2302
2303        const from = this.offset + this.rendered;
2304        const next = this.matched.slice(from, from + this.pageSize);
2305        if (!next.length) return;
2306
2307        const missing = next.filter((no) => !this.cards.has(no));
2308        if (!missing.length) {
2309            this.appendCards(next);
2310            return;
2311        }
2312
2313        this.loading = true;
2314        const loader = $("#productListLoader");
2315        loader.addClass("loading");
2316
2317        $.ajax({
2318            method: "POST",
2319            url: "/api/categoryv2/products",
2320            contentType: "application/json",
2321            dataType: "json",
2322            data: JSON.stringify({ nos: missing }),
2323        })
2324            .done((response) => {
2325                this.storeCards(response.html);
2326                this.appendCards(next);
2327            })
2328            .fail(() => {
2329                loader.addClass("error");
2330                setTimeout(() => loader.removeClass("error"), 3000);
2331            })
2332            .always(() => {
2333                this.loading = false;
2334                loader.removeClass("loading");
2335                this.onScroll();
2336            });
2337    },
2338
2339    storeCards(html) {
2340        if (!html) return;
2341        $($.parseHTML(html)).each((i, node) => {
2342            const card = $(node);
2343            if (!card.hasClass("ProductItem")) return;
2344            this.cards.set(card.attr("data-sku"), card);
2345        });
2346    },
2347
2348    appendCards(nos) {
2349        nos.forEach((no) => {
2350            const card = this.cards.get(no);
2351            if (card) this.listFor(no).append(card);
2352        });
2353        this.rendered += nos.length;
2354
2355        if (typeof lazyload === "function") lazyload();
2356        AddToCartBtn();
2357        incDecInit();
2358        if (typeof favoritesButtonInit === "function") favoritesButtonInit();
2359
2360        this.updateState();
2361    },
2362
2363    updateTotal() {
2364        if (this.groupTemplate) this.countGroups();
2365        const total = $(".TotalProducts");
2366        if (!total.length) return;
2367        total.html(this.matched.length + " " + total.data("label"));
2368    },
2369
2370    updateState() {
2371        $("#productListLoader").toggleClass("active", this.offset + this.rendered < this.matched.length);
2372        $(".emptyFilterResult").toggleClass("active", this.matched.length === 0);
2373    },
2374
2375    onScroll() {
2376        if (!this.active || this.loading) return;
2377        if (this.offset + this.rendered >= this.matched.length) return;
2378
2379        const loader = $("#productListLoader");
2380        if (!loader.length) return;
2381
2382        if ($(window).scrollTop() + $(window).height() + 300 >= loader.offset().top) {
2383            this.renderNext();
2384        }
2385    },
2386};
2387
2388function sortDropdownInit() {
2389    defaultSortingMethod = $(sortDropdown).attr("default");
2390    if (defaultSortingMethod == "") defaultSortingMethod = "name-az";
2391    if (!sortingMethod) {
2392        sortingMethod = defaultSortingMethod;
2393    }
2394    $(sortDropdown).val(sortingMethod);
2395    $(sortDropdown).on("change", function () {
2396        setUrlParams();
2397    });
2398}
2399function galleryViewInit() {
2400    $(galleryClasses).each(function () {
2401        $(`.btn-layout:has([value="${this}"])`).css("display", "flex");
2402    });
2403    $(".GalleryBox").attr("class", "GalleryBox " + currentGalleryClass);
2404    $(".btn-layout:has([value=" + currentGalleryClass + "]) input").attr("checked", "");
2405}
2406function niceSelectInit() {
2407    !(function (e) {
2408        e.fn.niceSelect = function (t) {
2409            var listHeight;
2410            function s(t) {
2411                t.after(
2412                    e("<div></div>")
2413                        .addClass("nice-select")
2414                        .addClass(t.attr("class") || "")
2415                        .addClass(t.attr("disabled") ? "disabled" : "")
2416                        .attr("tabindex", t.attr("disabled") ? null : "0")
2417                        .html('<span class="current"></span><ul class="list"></ul>'),
2418                );
2419                let s = t.next(),
2420                    n = t.find("option"),
2421                    i = t.find("option:selected");
2422                (s.find(".current").html(i.data("display") || i.text()),
2423                    n.each(function (t) {
2424                        let n = e(this),
2425                            i = n.data("display");
2426                        let stock = n.data("stock") != null ? n.data("stock") : "";
2427                        let pricetag = n.data("pricetag") != null ? n.data("pricetag") : "";
2428                        let stockText = n.data("stocktext") != null ? n.data("stocktext") : "";
2429                        let text = n.text();
2430                        s.find("ul").append(
2431                            e("<li></li>")
2432                                .attr("data-value", n.val())
2433                                .attr("pricetag", n.data("pricetag"))
2434                                .attr("stock", n.data("stock"))
2435                                .attr("stocktext", n.data("stocktext"))
2436                                .attr("data-display", i || null)
2437                                .addClass(
2438                                    "option" +
2439                                        (n.is(":selected") ? " selected" : "") +
2440                                        (n.is(":disabled") ? " disabled" : "") +
2441                                        (n.attr("class") ? " " + n.attr("class") : ""),
2442                                )
2443                                .html(
2444                                    "<span class='valuetext'>" +
2445                                        text +
2446                                        "</span>" +
2447                                        "<span class='stock'>" +
2448                                        stock +
2449                                        "</span><span class='pricetag'>" +
2450                                        pricetag +
2451                                        " </span><span class='stocktext'>" +
2452                                        stockText +
2453                                        "</span>",
2454                                ),
2455                        );
2456                    }));
2457                listHeight = s.find("ul").outerHeight();
2458            }
2459            if ("string" == typeof t)
2460                return (
2461                    "update" == t
2462                        ? this.each(function () {
2463                              let t = e(this),
2464                                  n = e(this).next(".nice-select"),
2465                                  i = n.hasClass("open");
2466                              n.length && (n.remove(), s(t), i && t.next().trigger("click"));
2467                          })
2468                        : "destroy" == t
2469                          ? (this.each(function () {
2470                                let t = e(this),
2471                                    s = e(this).next(".nice-select");
2472                                s.length && (s.remove(), t.css("display", ""));
2473                            }),
2474                            0 == e(".nice-select").length && e(document).off(".nice_select"))
2475                          : console.log('Method "' + t + '" does not exist.'),
2476                    this
2477                );
2478            (this.hide(),
2479                this.each(function () {
2480                    let t = e(this);
2481                    t.next().hasClass("nice-select") || s(t);
2482                }),
2483                e(document).off(".nice_select"),
2484                e(document).on("click.nice_select", ".nice-select", function (t) {
2485                    let s = e(this);
2486                    e(".nice-select").not(s).removeClass("open");
2487                    s.toggleClass("open");
2488                    if (s.hasClass("open")) {
2489                        (s.find(".option"), s.find(".focus").removeClass("focus"), s.find(".selected").addClass("focus"));
2490
2491                        if ($(".mobileTrue").length) {
2492                            return;
2493                        }
2494                        const navbarIntrusion = Math.max(
2495                            0,
2496                            $("#primary_nav").outerHeight() - ($(window).scrollTop() - $("#primary_nav").offset().top),
2497                        );
2498                        const spaceAbove = s.offset().top - $(window).scrollTop() - navbarIntrusion;
2499                        const footerIntrusion = Math.max(0, $(window).scrollTop() + $(window).height() - $("footer").offset().top);
2500                        const spaceBelow =
2501                            $(window).height() - (s.offset().top - $(window).scrollTop() + s.outerHeight()) - footerIntrusion;
2502                        s.find("ul").removeClass("open-upwards");
2503                        if (listHeight < spaceBelow - 20) return;
2504                        if (spaceAbove > spaceBelow) {
2505                            s.find("ul").addClass("open-upwards");
2506                        }
2507                        if (listHeight < spaceAbove - 20) {
2508                            return;
2509                        }
2510                        const maxHeight = Math.max(spaceBelow, spaceAbove) - 20;
2511                        s.find("ul").css("max-height", maxHeight + "px");
2512                    } else {
2513                        s.trigger("focus");
2514                    }
2515                }),
2516                e(document).on("click.nice_select", function (t) {
2517                    0 === e(t.target).closest(".nice-select").length && e(".nice-select").removeClass("open").find(".option");
2518                }),
2519                e(document).on("click.nice_select", ".nice-select .option:not(.disabled)", function (t) {
2520                    let s = e(this),
2521                        n = s.closest(".nice-select");
2522                    (n.find(".selected").removeClass("selected"), s.addClass("selected"));
2523                    let i = s.data("display") || s.find(".valuetext").text();
2524                    (n.find(".current").text(i), n.prev("select").val(s.data("value")).trigger("change"));
2525                }),
2526                e(document).on("keydown.nice_select", ".nice-select", function (t) {
2527                    let s = e(this),
2528                        n = e(s.find(".focus") || s.find(".list .option.selected"));
2529                    if (t.key === "Enter") return (s.hasClass("open") ? n.trigger("click") : s.trigger("click"), !1);
2530                    if (t.key === "ArrowDown") {
2531                        if (s.hasClass("open")) {
2532                            let i = n.nextAll(".option:not(.disabled)").first();
2533                            i.length > 0 && (s.find(".focus").removeClass("focus"), i.addClass("focus"));
2534                        } else {
2535                            s.trigger("click");
2536                        }
2537                        return !1;
2538                    }
2539                    if (t.key === "ArrowUp") {
2540                        if (s.hasClass("open")) {
2541                            let l = n.prevAll(".option:not(.disabled)").first();
2542                            l.length > 0 && (s.find(".focus").removeClass("focus"), l.addClass("focus"));
2543                        } else {
2544                            s.trigger("click");
2545                        }
2546                        return !1;
2547                    }
2548                    if (t.key === "Escape") s.hasClass("open") && s.trigger("click");
2549                    else if (t.key === "Tab" && s.hasClass("open")) return !1;
2550                }));
2551            2;
2552            let n = document.createElement("a").style;
2553            return ((n.cssText = "pointer-events:auto"), "auto" !== n.pointerEvents && e("html").addClass("no-csspointerevents"), this);
2554        };
2555    })(jQuery);
2556}
2557function searchSelectInit() {
2558    !(function (e) {
2559        e.fn.searchSelect = function () {
2560            const select = $(this);
2561            const input = $(
2562                `<input type="text" class="searchSelectInput" placeholder="${select.find(".option.selected .valuetext").text()}">`,
2563            );
2564            $(this).append(input);
2565            $(input)
2566                .on("focus", function (e) {
2567                    $(this).val("").trigger("input");
2568                    $(this).data("placehodler", $(this).attr("placeholder"));
2569                    $(this).attr("placeholder", "Type to search...");
2570                })
2571                .on("keydown", function (e) {
2572                    if (e.key === "Escape") {
2573                        $(this).trigger("blur");
2574                        return;
2575                    }
2576                    if (e.key === "ArrowDown" || e.key === "ArrowUp") {
2577                        const selected = select.find(".option:visible").filter(".active");
2578                        let next = null;
2579                        if (e.key === "ArrowDown") {
2580                            next = selected.nextAll(".option:visible").first();
2581                        } else {
2582                            next = selected.prevAll(".option:visible").first();
2583                        }
2584                        if (!next.length) {
2585                            if (e.key === "ArrowDown") {
2586                                next = select.find(".option:visible").first();
2587                            } else {
2588                                next = select.find(".option:visible").last();
2589                            }
2590                        }
2591                        selected.removeClass("active");
2592                        next.addClass("active");
2593                        const list = select.find(".list");
2594                        if (next.offset().top < list.offset().top) {
2595                            list.scrollTop(list.scrollTop() + next.offset().top - list.offset().top);
2596                        } else if (next.offset().top + next.outerHeight() > list.offset().top + list.outerHeight()) {
2597                            list.scrollTop(
2598                                list.scrollTop() + next.offset().top + next.outerHeight() - (list.offset().top + list.outerHeight()),
2599                            );
2600                        }
2601                        return;
2602                    }
2603                    if (e.key === "Enter") {
2604                        e.preventDefault();
2605                        e.stopPropagation();
2606                        const selected = select.find(".option:visible").filter(".active");
2607                        if (selected.length) {
2608                            selected.trigger("click");
2609                        }
2610                        return;
2611                    }
2612                })
2613                .on("input", function (e) {
2614                    const searchTerm = $(this).val().toLowerCase();
2615                    if (searchTerm === "") {
2616                        select.removeClass("filter");
2617                        select.find(".option").show();
2618                        return;
2619                    }
2620                    
2621                    select.addClass("filter");
2622                    const shown = select.find(".option").filter(function () {
2623                        const show =
2624                            $(this).text().toLowerCase().indexOf(searchTerm) > -1 ||
2625                            $(this).attr("data-value")?.toLowerCase().indexOf(searchTerm) > -1;
2626                        $(this).toggle(show);
2627                        return show;
2628                    });
2629                    shown.first().addClass("active").siblings().removeClass("active");
2630                })
2631                .on("blur", function () {
2632                    $(this).attr("placeholder", $(this).data("placehodler"));
2633                })
2634        };
2635    })(jQuery);
2636}
2637function ajaxQueueInit() {
2638    (function ($) {
2639        // jQuery on an empty object, we are going to use this as our Queue
2640        let ajaxQueue = $({});
2641        $.ajaxQueue = function (ajaxOpts) {
2642            let jqXHR,
2643                dfd = $.Deferred(),
2644                promise = dfd.promise();
2645
2646            // queue our ajax request
2647            ajaxQueue.queue(doRequest);
2648
2649            // add the abort method
2650            promise.abort = function (statusText) {
2651                // proxy abort to the jqXHR if it is active
2652                if (jqXHR) {
2653                    return jqXHR.abort(statusText);
2654                }
2655                // if there wasn't already a jqXHR we need to remove from queue
2656                let queue = ajaxQueue.queue(),
2657                    index = $.inArray(doRequest, queue);
2658
2659                if (index > -1) {
2660                    queue.splice(index, 1);
2661                }
2662                // and then reject the deferred
2663                dfd.rejectWith(ajaxOpts.context || ajaxOpts, [promise, statusText, ""]);
2664                return promise;
2665            };
2666
2667            // run the actual query
2668            function doRequest(next) {
2669                jqXHR = $.ajax(ajaxOpts).done(dfd.resolve).fail(dfd.reject).then(next, next);
2670            }
2671            return promise;
2672        };
2673    })(jQuery);
2674}
2675function favoritesButtonInit() {
2676    if ($(".favoriteStarButton").length) {
2677        $(".favoriteStarButton").on("click", function () {
2678            toggleFavoritesDropdown($(this));
2679        });
2680        $("body").on("click", function (event) {
2681            //Hide the favorites dropdown if clicked outside
2682            if (!$(event.target).closest(".favoriteStarButton").length && !$(event.target).closest(".favoritesDropdown").length) {
2683                $(".showDropdown").removeClass("showDropdown");
2684            }
2685        });
2686    }
2687}
2688
2689// --- BASKET --- //
2690function parseBasket(data) {
2691    if (data == "") {
2692        $("body").removeClass("loading");
2693        return;
2694    }
2695
2696    let decimalnum = (0.1).toString();
2697    let decmialstr = decimalnum.substring(1, 2);
2698
2699    $(".dynBasketQuantity").text(
2700        data.salesQuoteLines.reduce(function (_this, val) {
2701            if (showBasketQtySum == "0") {
2702                if (val.lineObjectNumber != freightitem) {
2703                    $("body").removeClass("loading");
2704                    return Math.round((parseFloat(_this) + 1 + Number.EPSILON) * 100) / 100;
2705                } else {
2706                    $("body").removeClass("loading");
2707                    return _this;
2708                }
2709            } else {
2710                if (val.lineObjectNumber != freightitem) {
2711                    $("body").removeClass("loading");
2712                    return Math.round((parseFloat(_this) + val.quantity + Number.EPSILON) * 100) / 100;
2713                } else {
2714                    $("body").removeClass("loading");
2715                    return Math.round((_this + Number.EPSILON) * 100) / 100;
2716                }
2717            }
2718        }, 0),
2719    );
2720
2721    $(".dynBasketQuantity").text($(".dynBasketQuantity").text().replace(decmialstr, DecimalSeparator));
2722
2723    $(".topBarBasketSection").addClass("changed");
2724    setTimeout(function () {
2725        $(".topBarBasketSection").removeClass("changed");
2726    }, basketChangedTimeout);
2727
2728    let AllLines = [];
2729
2730    if ($("body").hasClass("pageViewBasket")) {
2731        $(".pageViewBasket .RowQuickAddToCartLink").on("click", function (e) {
2732            $(this).animate(
2733                {
2734                    opacity: "0",
2735                },
2736                200,
2737            );
2738            if (!quickAddInitialized) quickAddInit();
2739            $(".basketRowLines_QuickAddToCart *").val("");
2740            $("html, body").animate(
2741                {
2742                    scrollTop: "1px",
2743                },
2744                400,
2745            );
2746
2747            $(".basketRowLines_QuickAddToCart").animate(
2748                {
2749                    maxHeight: "100px",
2750                },
2751                400,
2752            ).css('margin-block', 'var(--gapS) var(--gapSM)');
2753            setTimeout(function () {
2754                $(".basketRowLines_QuickAddToCart").css("overflow", "visible");
2755            }, 600);
2756            $("#quicksku").focus();
2757        });
2758
2759        let reloadBasket = false;
2760
2761        if (data.salesQuoteLines.length > 0) {
2762            $(data.salesQuoteLines).each(function (k, v) {
2763                if ($('.syncItem[itemNo="' + v.id + '"]').length == 0) {
2764                    reloadBasket = true;
2765                }
2766                AllLines.push(v.id);
2767            });
2768        } else if ($(".syncItem").length == 0) {
2769            $("body.loading").removeClass("loading");
2770        }
2771
2772        $(".syncItem").each(function () {
2773            if (!AllLines.includes($(this).attr("itemNo"))) {
2774                reloadBasket = true;
2775            }
2776        });
2777
2778        if (reloadBasket) {
2779            reloadBasketPage(function () {
2780                $("body").removeClass("loading");
2781            });
2782        }
2783    }
2784
2785    //Update basket lines
2786    if (!$(".dynBasketLine.showFieldTrue").length) {
2787        $(data.salesQuoteLines).each(function (k, v) {
2788            const elm = $('.basketRowLine[itemNo="' + v.id + '"]');
2789            elm.find(".Text_Name").html(v.description);
2790            $(".dynBasketUnitPrice" + v.id).text(v.unitPrice);
2791            if (v.discountPercent > 0) {
2792                //Here used to be the line below, but this do not work when the discount is a percentage...
2793                //$('.dynBasketDiscountPercent' + v.id).text(v.discountPercent + '%');
2794                $(".dynBasketDiscountPercent" + v.id).text(v.discountPercent + "%");
2795            } else {
2796                $(".dynBasketDiscountPercent" + v.id).text("");
2797            }
2798            $(".dynBasketTotalPrice" + v.id).text(v.amountExcludingTax);
2799        });
2800    }
2801
2802    $(".dynBasketTotal_ExcludeTax").text(data.totalAmountExcludingTax);
2803    $(".dynBasketTotal_DiscountAmount").text(data.discountAmount);
2804    $(".dynBasketTotal_PureTax").text(data.totalTaxAmount);
2805    $(".dynBasketTotal_IncludeTax").text(data.totalAmountIncludingTax);
2806
2807    onBasketParsed(data);
2808}
2809function reloadBasketPage(callback) {
2810    $.ajax({
2811        url: "/basket/",
2812        success: function (data) {
2813            $(".basketRowLines").html($(data).find(".basketRowLines").html());
2814            $(".basketlinehash").val($(data).find(".basketlinehash").val());
2815
2816            lazyload();
2817            incDecInit(true);
2818            if (callback != null) {
2819                callback();
2820            }
2821            onBasketUpdated();
2822        },
2823    });
2824}
2825function setBasketInfo() {
2826    $.ajax({
2827        method: "POST",
2828        url: "/api/basket/info/set",
2829        cache: false,
2830        data: $(".RenderBody *:visible").serialize(),
2831    })
2832        .done(function (data) {})
2833        .fail(function () {})
2834        .always(function () {});
2835}
2836function getBasketInfo() {
2837    $.ajax({
2838        method: "GET",
2839        url: "/api/basket/info/get",
2840        cache: false,
2841    })
2842        .done(function (data) {
2843            $.each(JSON.parse(data), function (k, v) {
2844                if (v == "true")
2845                    $('*[name="' + k + '"]')
2846                        .attr("checked", "checked")
2847                        .trigger("change");
2848                else
2849                    $('*[name="' + k + '"]')
2850                        .val(v)
2851                        .trigger("change");
2852            });
2853        })
2854        .fail(function () {})
2855        .always(function () {
2856            $(".scShopCheckout input, .scShopCheckout select, .scShopCheckout textarea").on("change", function (e) {
2857                setBasketInfo();
2858            });
2859        });
2860}
2861function incDecInit(reload = false, selector = null) {
2862    selector = selector || (reload ? $(".basketRowLines input.addIncDec") : $(".addIncDec"));
2863
2864    // Replace input elements with increment/decrement buttons
2865    $(selector).each(function () {
2866        const decBtn = $('<div class="input-group-text decButton">-</div>');
2867        const incBtn = $('<div class="input-group-text incButton">+</div>');
2868
2869        $(this)
2870            .removeClass("addIncDec")
2871            .addClass("incDecInput")
2872            .attr("onblur", "checkAmountValidity(this)")
2873            .wrap('<div class="input-group input-group-sm IncDecSection flex-nowrap"></div>')
2874            .before(decBtn)
2875            .after(incBtn);
2876
2877        $(this).on("blur", function () {
2878            checkMinMaxDecimals(this);
2879        });
2880
2881        // Handle click events for increment/decrement buttons
2882        $(decBtn)
2883            .off()
2884            .on("click", function (e) {
2885                e.preventDefault();
2886                adjustValue(this, false);
2887            });
2888        $(incBtn)
2889            .off()
2890            .on("click", function (e) {
2891                e.preventDefault();
2892                adjustValue(this, true);
2893            });
2894        $(this)
2895            .on("click", function (e) {
2896                e.preventDefault();
2897            })
2898            .on("keydown", function (e) {
2899                if (e.key === "ArrowUp") {
2900                    e.preventDefault();
2901                    adjustValue(this, true);
2902                } else if (e.key === "ArrowDown") {
2903                    e.preventDefault();
2904                    adjustValue(this, false);
2905                }
2906            });
2907    });
2908
2909    // to be deprecated
2910    $(".StairStepPricing")
2911        .closest("div:has(.ProductAmountInput)")
2912        .find(".ProductAmountInput")
2913        .each(function () {
2914            $(this).off("change keyup");
2915            $(this).on("change keyup", function () {
2916                let stairseps = $(this).closest("div:has(.StairStepPricing)").find(".StairStepPricing");
2917                const form = $(this);
2918                $(stairseps).each(function () {
2919                    let tbody = $(this).find("tbody");
2920                    let rows = $(tbody).find("tr");
2921                    let qtyPricePairs = [];
2922                    let amount = $(form).val();
2923                    if (!amount || parseInt(amount) < 1) return;
2924                    $(rows).each(function () {
2925                        let qty = $(this).find(".qty").text().trim();
2926                        let price = $(this).find(".price").text().trim();
2927                        qtyPricePairs.push({ qty: qty, price: price });
2928                    });
2929                    let i = rows.length - 1;
2930                    let qtyRow = null;
2931                    while (qtyRow == null) {
2932                        let qty = qtyPricePairs[i].qty;
2933                        if (parseInt(amount) >= parseInt(qty)) {
2934                            qtyRow = i;
2935                            break;
2936                        }
2937                        i--;
2938                    }
2939                    $(tbody).attr("data-active", qtyRow);
2940
2941                    $(rows[qtyRow]).siblings().removeClass("active");
2942
2943                    if (qtyRow > 0) {
2944                        $(rows[qtyRow]).addClass("active");
2945                    }
2946                    if (!$(rows[qtyRow]).find(".price").length) return;
2947                    let priceSpan = $(form).parents(".InteractRow").siblings(".PriceRow").find('[replaceKey="GetPriceTag"]');
2948                    if (!priceSpan.length) {
2949                        priceSpan = $(form).parents(".ProductBuy").find('[replaceKey="GetPriceTag"]');
2950                    }
2951                    let newPrice = $(rows[qtyRow]).find(".price").html();
2952                    priceSpan.html(newPrice);
2953                });
2954            });
2955        });
2956}
2957function deleteProductFromBasket(elm, basketGuid, lineGuid) {
2958    elm.removeClass("scError");
2959
2960    let qty = elm.val();
2961
2962    let line = $(elm).parents(".basketRowLine");
2963    let height = $(line).outerHeight();
2964    $(line)
2965        .addClass("deleting")
2966        .css({ height: height + "px", "max-height": height + "px" });
2967    setTimeout(function () {
2968        $(line).html("").removeClass("deleting").addClass("deleted");
2969        setTimeout(function () {
2970            $(line).remove();
2971        }, 350);
2972    }, 350);
2973
2974    $.ajax({
2975        method: "POST",
2976        url: "/api/basket/edit/" + basketGuid + "/lines/" + lineGuid + "/remove/",
2977        cache: false,
2978    })
2979        .done(function (data) {
2980            parseBasket(data);
2981            if (!data.salesQuoteLines.length) {
2982                window.location.reload();
2983            }
2984            onProductRemoved();
2985        })
2986        .fail(function () {
2987            elm.addClass("scError");
2988        })
2989        .always(function () {});
2990}
2991function UpdateOrderDate(elm) {
2992    $("#completeOrder").prop("disabled", true);
2993    $("#completeOrder").text(WaitMessage);
2994    $("body").addClass("loading");
2995    $.ajax({
2996        method: "POST",
2997        url: "/api/basket/updateorderdate/" + $(elm).val(),
2998        cache: false,
2999        dataType: "json",
3000        contentType: "application/json",
3001    })
3002        .done(function (data) {})
3003        .fail(function (jqXHR, textStatus) {})
3004        .always(function () {
3005            document.location.href = document.location.href.split("#")[0];
3006        });
3007}
3008// Comments
3009function openComment(elm) {
3010    let sourceRow = $(elm).find('.row[class*="CommentLine_"]');
3011    let modalElm = $(".modal-commentline").first();
3012    modalElm.find(".cloneRow:not(.d-none)").remove();
3013    let cloneRow = modalElm.find(".cloneRow");
3014    new bootstrap.Modal(modalElm, {
3015        keyboard: true,
3016    });
3017    sourceRow.each(function (k, v) {
3018        let cloneEditRow = cloneRow.clone();
3019        let input = cloneEditRow.find("input");
3020        let label = cloneEditRow.find("label");
3021        /*CONTENT*/
3022        // cleanup remove all old data tags
3023        $.each(input.data(), function (i) {
3024            input.removeAttr("data-" + i);
3025        });
3026        // copy all data tags from source to target
3027        $.each($(v).data(), function (i, v) {
3028            input.attr("data-" + i, v);
3029        });
3030
3031        let MDValue = $(v).data("masterdatavalue");
3032        label.text($(v).data("key")).attr("for", MDValue);
3033        input.val($(v).data("value")).attr("id", MDValue);
3034
3035        if (MDValue.endsWith("DATE")) {
3036            input.attr("type", "DATE");
3037        } else if (MDValue.endsWith("SELECT")) {
3038            let id = $(v).data("lineid");
3039            let selecthtml = $(".LineSelectLineId" + MDValue + id).prop("outerHTML");
3040            selecthtml = selecthtml.replace("d-none", "");
3041            input.replaceWith(selecthtml);
3042        } else if (MDValue.endsWith("FILE") || MDValue.endsWith("IMAGE")) {
3043            input.attr("type", "FILE");
3044            input.on("change", function () {
3045                cartCommentFileChanged(input);
3046            });
3047            let wrapper = $("<div>").addClass("fileInputWrapper");
3048            $(input).wrap($(wrapper));
3049            let orgValue = $(input).data("orgvalue");
3050            if (orgValue) {
3051                $(input).after($("<span>").html(orgValue.split("\\").pop()));
3052            }
3053        } else {
3054            input.attr("type", "TEXT");
3055        }
3056        /*CONTENT*/
3057        cloneEditRow.removeClass("d-none");
3058        cloneEditRow.insertBefore(cloneRow);
3059    });
3060    $(".modal-commentline").first().modal("show");
3061}
3062function saveHeaderComment(elm) {
3063    if (!hasFired) {
3064        hasFired = true;
3065        let postData = new Array();
3066        let bAnyNewLines = false;
3067
3068        $("#completeOrder").prop("disabled", true);
3069        $("#completeOrder").text(WaitMessage);
3070
3071        $(".HeaderComment").each(function () {
3072            let value = $(this).val();
3073            let orgvalue = $(this).data("orgvalue");
3074
3075            if (value != orgvalue) {
3076                $(this).data("orgvalue", value);
3077
3078                cl = new CommentLine();
3079                cl.salesquoteid = $(this).data("salesquoteid");
3080                cl.lineid = 0;
3081                cl.lineno = $(this).data("lineno").toString();
3082                cl.masterdatacode = $(this).data("masterdatacode");
3083                cl.masterdatavalue = $(this).data("masterdatavalue");
3084                cl.value = encodeURIComponent(value);
3085                cl.isfile = "";
3086                cl.filedata = "";
3087                postData.push(cl);
3088                bAnyNewLines = true;
3089            }
3090        });
3091
3092        if (bAnyNewLines) {
3093            $("body").addClass("loading");
3094            $.ajax({
3095                method: "POST",
3096                url: "/api/basket/updateheadercomments/",
3097                cache: false,
3098                data: JSON.stringify(postData),
3099                contentType: "application/json",
3100            })
3101                .done(function (data) {})
3102                .fail(function (jqXHR, textStatus) {})
3103                .always(function (data) {
3104                    if (data == "1" || data == "reload") {
3105                        document.location.href = document.location.href.split("#")[0];
3106                    } else {
3107                        $("body").removeClass("loading");
3108                        $("#completeOrder").prop("disabled", false);
3109                        $("#completeOrder").text(CompleteMessage);
3110                        hasFired = false;
3111                    }
3112                });
3113        } else {
3114            $("body").removeClass("loading");
3115            $("#completeOrder").prop("disabled", false);
3116            $("#completeOrder").text(CompleteMessage);
3117            hasFired = false;
3118        }
3119    }
3120}
3121function saveCommentLines() {
3122    let postData = new Array();
3123    let bAnyNewLines = false;
3124
3125    $(".modal-commentline .cloneRow:not(.d-none) input.basketLineComments").each(function () {
3126        let value = $(this).val();
3127        let orgvalue = $(this).data("orgvalue");
3128        let hasFile = $(this).data("hasfile");
3129        let fileData = $(this).data("filedata");
3130        let type = $(this).attr("type");
3131
3132        if (hasFile || (value != orgvalue && type != "FILE")) {
3133            let cl = new CommentLine();
3134            cl.salesquoteid = $(this).data("salesquoteid");
3135            cl.lineid = $(this).data("lineid");
3136            cl.lineno = $(this).data("lineno").toString();
3137            cl.masterdatacode = $(this).data("masterdatacode");
3138            cl.masterdatavalue = $(this).data("masterdatavalue");
3139            cl.value = encodeURIComponent(value.split("\\").pop());
3140            cl.isfile = hasFile ? "true" : "";
3141            cl.filedata = fileData || "";
3142            postData.push(cl);
3143            bAnyNewLines = true;
3144        }
3145    });
3146    $(".modal-commentline .cloneRow:not(.d-none) select.basketLineComments").each(function () {
3147        let value = $(this).find(":selected").val();
3148        let orgvalue = $(this).data("orgvalue");
3149        let type = $(this).attr("type");
3150
3151        if (value != orgvalue) {
3152            let cl = new CommentLine();
3153            cl.salesquoteid = $(this).data("salesquoteid");
3154            cl.lineid = $(this).data("lineid");
3155            cl.lineno = $(this).data("lineno").toString();
3156            cl.masterdatacode = $(this).data("masterdatacode");
3157            cl.masterdatavalue = $(this).data("masterdatavalue");
3158            cl.value = encodeURIComponent(value.split("\\").pop());
3159            cl.isfile = "";
3160            cl.filedata = "";
3161            postData.push(cl);
3162            bAnyNewLines = true;
3163        }
3164    });
3165
3166    if (bAnyNewLines) {
3167        $("body").addClass("loading");
3168        $.ajax({
3169            method: "POST",
3170            url: "/api/basket/updatelinecomments/" + $(".basketlinehash").val(),
3171            cache: false,
3172            dataType: "json",
3173            data: JSON.stringify(postData),
3174            contentType: "application/json",
3175        }).always(function (jqXHR) {
3176            if (jqXHR.status == 200) {
3177                $(".modal-commentline").first().modal("hide");
3178                setTimeout(function () {
3179                    reloadBasketPage(function () {
3180                        $("body").removeClass("loading");
3181                    });
3182                }, 800);
3183            }
3184        });
3185    } else {
3186        $(".modal-commentline").first().modal("hide");
3187    }
3188}
3189// Quick add by SKU
3190function quickAddInit() {
3191    $(".basketRowLines_QuickAddToCart .incDecInput").on("keydown", function (e) {
3192        if (e.keyCode == 13) {
3193            if ($(this).val() > 0) $(".basketRowLines_QuickAddToCart .addToCartAni").trigger("mouseup");
3194        }
3195    });
3196    $("#quicksku").on("keyup", function (e) {
3197        if (e.keyCode == 13) {
3198            $(this).trigger("blur");
3199            quickskucheck($(this));
3200        } else {
3201            quickskucheck($(this), false);
3202        }
3203    });
3204    $(document).on("click", function () {
3205        $(".quickadderror").html("").hide();
3206    });
3207    quickAddInitialized = true;
3208}
3209function quickskucheck(elm, finalCheck = true) {
3210    const sku = $(elm).val();
3211    if (sku == "") return;
3212    if (!finalCheck) $(".quickadderror").html("").hide();
3213    $.ajax({
3214        method: "GET",
3215        url: "/shop/api/GetProduct/?productId=" + encodeURIComponent(sku) + "&$fields=variantsBc,variantDescription,no,uom,minQty,colliQty",
3216        cache: false,
3217        dataType: "json",
3218    })
3219        .done(function (data) {
3220            $(elm).addClass("success");
3221            if (data.value.variantsBc.length > 0) {
3222                $(".basketRowLines_QuickAddToCart:not(#quicksku)").val("");
3223                $(".RowQuickAddToCart_quickvariant").show();
3224                let loopOption = "";
3225                $.each(data.value.variantsBc, function (i, variant) {
3226                    loopOption +=
3227                        "<option value='" + variant.no + "' data-min-qty='" + variant.minQty + "' data-colli-qty='" + variant.colliQty + "'>" + variant.variantDescription + "</option>";
3228                });
3229
3230                //$('.dropdownVariant').html('<select name="quickvariant" id="quickvariant" onselect="return quickvariantcheck()" class="form-control form-control-sm">' + loopOption + '</select>');
3231                $(".dropdownVariant").html(
3232                    '<select name="quickvariant" id="quickvariant" class="form-control form-control-sm">' + loopOption + "</select>",
3233                );
3234
3235                $("#quickvariant").on("change", applySelectedVariantQtyLimits);
3236                applySelectedVariantQtyLimits();
3237
3238                $('*[name="quickvariant"]').select2({
3239                    tags: true,
3240                });
3241
3242                if (!finalCheck) return;
3243
3244                $(document).on("select2:open", () => {
3245                    document.querySelector(".select2-search__field").focus();
3246                });
3247
3248                $(document).on("select2:select", () => {
3249                    $('*[name="quickqty"]').trigger("focus");
3250                });
3251
3252                $('*[name="quickvariant"]').select2("open");
3253            } else {
3254                setQuickQtyLimits(data.value.minQty, data.value.colliQty);
3255                if (finalCheck) {
3256                    $(".basketRowLines_QuickAddToCart .amount input").focus();
3257                }
3258            }
3259            $(".quickadderror").html("").hide();
3260            $('input[name="quickuom"]').val(data.value.uom);
3261        })
3262        .fail(function () {
3263            $(elm).removeClass("success");
3264            $("#quickvariant").prop("disabled", true).html("");
3265            setQuickQtyLimits();
3266            if (!finalCheck) return;
3267            $(".quickadderror").html(SKUNotFound).show();
3268            $('input[name="quicksku"]').val("");
3269            $('input[name="quicksku"]').trigger("focus");
3270        });
3271}
3272function setQuickQtyLimits(minQty = 0, colliQty = 1) {
3273    $('input[name="quickqty"]').attr({ min: minQty, step: colliQty });
3274}
3275function applySelectedVariantQtyLimits() {
3276    const selectedVariant = $("#quickvariant option:selected");
3277    setQuickQtyLimits(selectedVariant.data("min-qty"), selectedVariant.data("colli-qty"));
3278}
3279function quickaddtocart(elm) {
3280    let qtyInput = $('input[name = "quickqty"]');
3281    checkMinMaxDecimals(qtyInput);
3282
3283    let sku = $('input[name="quicksku"]').val();
3284    const variant = $('*[name="quickvariant"]').val();
3285    const qty = qtyInput.val();
3286    const variantDescription = $("#select2-quickvariant-container").text();
3287
3288    let uom = $('input[name="quickuom"]').val();
3289    let error = "";
3290
3291    if (!(sku && qty) || qty <= 0) {
3292        error = "Fill in all fields";
3293    }
3294    if (uom) {
3295        uom = "/uom/" + encodeURIComponent(uom);
3296    }
3297
3298    let itemVariantId = "";
3299
3300    if (variant) itemVariantId = "/itemvariantid/" + encodeURIComponent(variant);
3301
3302    $(elm).find(".addedToCartText").removeClass("d-none");
3303
3304    let spanContent = $(elm).find("span").html();
3305    $(elm).css({
3306        width: $(elm).outerWidth() + "px",
3307        height: $(elm).outerHeight() + "px",
3308    });
3309    $(elm).find("span").html("");
3310    $(elm).addClass("loading");
3311
3312    if (sku.indexOf("/") > 0) {
3313        sku = sku.replace("/", "%2F");
3314    }
3315
3316    const oldRowLines = $(".basketRowLines .basketRowLine");
3317    $.ajax({
3318        method: "POST",
3319        url: "/api/basket/add/" + encodeURIComponent(sku) + "/quantity/" + qty + uom + itemVariantId,
3320        cache: false,
3321        dataType: "json",
3322        contentType: "application/json",
3323    })
3324        .done(function (data) {
3325            //reset the quick add form
3326            $("#quickvariant").prop("disabled", true).html("");
3327            $(".quickadderror").html("").hide();
3328            $("#quicksku").val("").removeClass("success").focus();
3329            $(".basketRowLines_QuickAddToCart *").val("");
3330            $(elm).addClass("success").attr("last-class", "success");
3331
3332            //reload basket partial and define callback function
3333            reloadBasketPage(function () {
3334                for (i = 0; i < $(".basketRowLines .basketRowLine").length; i++) {
3335                    const line = $(".basketRowLines .basketRowLine").eq(i);
3336                    const oldLine = $(oldRowLines).eq(i);
3337                    if (line.attr("itemno") != oldLine.attr("itemno")) break;
3338                    if (line.find(".incDecInput").val() != oldLine.find(".incDecInput").val()) break;
3339                }
3340                $(".newLineAlert").removeClass("newLineAlert");
3341                $(".lineAdded").removeClass("lineAdded");
3342                const newLine = $(".basketRowLines .basketRowLine").eq(i);
3343                if (oldRowLines.length != $(".basketRowLines .basketRowLine").length) {
3344                    newLine.addClass("lineAdded");
3345                    let maxHeight = $(":root").css("--basketRowHeight");
3346                    if (!$(".screenW--xs--s--m--l").length) {
3347                        const interactH = $(":root").css("--interactHeight");
3348                        const basketPadding = $(":root").css("--basketRowPadding");
3349                        maxHeight = `calc(${maxHeight} + ${interactH} + 2 * ${basketPadding})`;
3350                    }
3351                    setTimeout(function () {
3352                        newLine.css("max-height", maxHeight);
3353                        newLine.removeClass("newLineAlert");
3354                    }, 300);
3355                }
3356                newLine.addClass("newLineAlert");
3357            });
3358        })
3359        .fail(function () {
3360            if (error == "") error = "Error adding item";
3361            $(".quickadderror").html(error).show();
3362            $(".basketRowLines_QuickAddToCart *").val("");
3363            $("#quicksku").focus();
3364            $(elm).addClass("error").attr("last-class", "error");
3365        })
3366        .always(function () {
3367            $(elm).removeClass("loading");
3368            setTimeout(function () {
3369                $(elm).removeClass("error success");
3370                if (spanContent) $(elm).find("span").html(spanContent);
3371                $(elm).css({ width: "", height: "" });
3372            }, 3000);
3373        });
3374} hideQuickAdd
3375function hideQuickAdd() {
3376    $(".RowQuickAddToCart").show();
3377    $(".basketRowLines_QuickAddToCart").css("overflow", "clip");
3378    $(".basketRowLines_QuickAddToCart").animate(
3379        {
3380            maxHeight: "0",
3381        },
3382        400,
3383    ).css('margin-block', "");
3384    $(".RowQuickAddToCartLink").animate(
3385        {
3386            opacity: "1",
3387        },
3388        400,
3389    );
3390}
3391function adjustValue(button, isIncrement) {
3392    if ($(button).closest(".interactDisabled").length) {
3393        return;
3394    }
3395    const input = $(button).parent().find("input");
3396    const step = parseInt($(input).attr("step")) || 1;
3397    const min = parseInt($(input).attr("min")) || 0;
3398    const max = parseInt($(input).attr("max"));
3399    const type = $(input).attr("type");
3400    const allowZero = $(input).attr("allow-zero") === "allow";
3401
3402    let containsComma = false;
3403    let decimalPointMultiplier = Math.pow(10, $(input).data("decimals"));
3404    if (!decimalPointMultiplier) decimalPointMultiplier = 1;
3405
3406    let value = $(input).val() || 0;
3407    if (type == "number") value = parseInt(value);
3408    if (type == "text") {
3409        containsComma = value.includes(",");
3410        value = parseFloat(value.replaceAll(",", "."));
3411    }
3412    value += isIncrement ? step : -step;
3413    value = Math.round(value * decimalPointMultiplier) / decimalPointMultiplier;
3414
3415    if (!isNaN(min) && value < min) {
3416        value = allowZero && !isIncrement ? 0 : min;
3417    }
3418    if (!isNaN(max) && value > max) value = max;
3419    if (type == "text") {
3420        if (containsComma) value = value.toString().replace(".", ",");
3421        else value = value.toString();
3422    }
3423    input.val(value).change();
3424}
3425// Salesquotes
3426function EmailQuotePdf() {
3427    let modalElm = $(".modal-quotepdf");
3428    modalElm.modal("show");
3429}
3430function SendQuotePdfEmail(quoteid) {
3431    $("body").addClass("loading");
3432    $.ajax({
3433        method: "POST",
3434        url: "/shop/api/emailquote/" + quoteid,
3435        cache: false,
3436        contentType: "application/x-www-form-urlencoded",
3437        data: $(".modal-quotepdf *:visible").serialize(),
3438    })
3439        .fail(function () {
3440            $("body").removeClass("loading");
3441
3442            alert("Failed to send email!");
3443        })
3444        .done(function (response) {
3445            $("body").removeClass("loading");
3446            let modalElm = $(".modal-quotepdf");
3447            modalElm.modal("hide");
3448        });
3449}
3450function mutlipleSalesQuotesInit() {
3451    if ($("#selectUserSalesQuote").val() == "new") {
3452        $("#createUserSalesQuote").removeClass("d-none");
3453        $("#salesQuoteDescription").removeClass("d-none").val("");
3454        $("#deleteUserSalesQuote").addClass("d-none");
3455        $("#renameUserSalesQuote").addClass("d-none");
3456    }
3457    $("#selectUserSalesQuote").on("change", function () {
3458        selectSalesQuote($(this).val());
3459    });
3460    $("#deleteUserSalesQuote").on("click", function () {
3461        deleteSalesQuote($("#selectUserSalesQuote").attr("selectedId"));
3462    });
3463    $("#createUserSalesQuote").on("click", function () {
3464        createUserQuote();
3465    });
3466    $("#renameUserSalesQuote").on("click", function () {
3467        renameButtonClick();
3468    });
3469    $("#salesQuoteDescription").on("keydown", function (e) {
3470        if (e.keyCode == 13) {
3471            if ($(".renameMode").length > 0) $("#renameUserSalesQuote").click();
3472            else $("#createUserSalesQuote").click();
3473        } else if (e.keyCode == 27) {
3474            $("#deleteUserSalesQuote").trigger("click");
3475        }
3476    });
3477}
3478function selectSalesQuote(val) {
3479    if (val == "new") {
3480        $("#selectUserSalesQuote").attr("lastSelected", $("#selectUserSalesQuote").attr("selectedId"));
3481        $("#createUserSalesQuote").removeClass("d-none");
3482        $("#salesQuoteDescription").removeClass("d-none").val("");
3483        $("#deleteUserSalesQuote").addClass("d-none");
3484        $("#renameUserSalesQuote").addClass("d-none");
3485    } else {
3486        if ($("#deleteUserSalesQuote").hasClass("d-none")) {
3487            $("#createUserSalesQuote").addClass("d-none");
3488            $("#salesQuoteDescription").addClass("d-none");
3489            $("#deleteUserSalesQuote").removeClass("d-none");
3490            $("#renameUserSalesQuote").removeClass("d-none");
3491        }
3492        if (val == $("#selectUserSalesQuote").attr("lastSelected")) return;
3493        $("body").addClass("loading");
3494
3495        $.ajax({
3496            method: "GET",
3497            url: "/api/basket/setselectedsalesquote?id=" + val,
3498            cache: false,
3499            dataType: "json",
3500            contentType: "application/json",
3501        })
3502            .done(function (data) {})
3503            .always(function (data) {
3504                parseBasket(data);
3505                let selected = $("#selectUserSalesQuote").find("option:selected");
3506                let newDesc = $(selected).attr("data-description");
3507                $("#salesQuoteDescription").val(newDesc).attr("value", newDesc);
3508                $("#selectUserSalesQuote").attr("selectedId", $(selected).attr("value"));
3509                $("#selectUserSalesQuote").attr("selectedNo", $(selected).data("quoteno"));
3510            });
3511    }
3512}
3513function createUserQuote() {
3514    let url = "/api/basket/createsalesquote?description=" + encodeURIComponent($("#salesQuoteDescription").val());
3515
3516    $("body").addClass("loading");
3517
3518    $.ajax({
3519        method: "POST",
3520        url: url,
3521        cache: false,
3522        dataType: "json",
3523        contentType: "application/json",
3524    })
3525        .done(function () {})
3526        .always(function () {
3527            window.location.href = url;
3528        });
3529}
3530function renameButtonClick() {
3531    $(".salesQuoteSection").addClass("renameMode");
3532    $("#salesQuoteDescription").trigger("focus").trigger("select");
3533    $("#renameUserSalesQuote")
3534        .off("click")
3535        .on("click", function () {
3536            renameSalesQuote();
3537        });
3538    $("#deleteUserSalesQuote")
3539        .off("click")
3540        .on("click", function () {
3541            $(".salesQuoteSection").attr("class", "salesQuoteSection");
3542            $("#renameUserSalesQuote")
3543                .off("click")
3544                .on("click", function () {
3545                    renameButtonClick();
3546                });
3547            $(this)
3548                .off("click")
3549                .on("click", function () {
3550                    document.location.href = "/api/basket/deleteselectedsalesquote?id=" + $("#selectUserSalesQuote").attr("selectedId");
3551                });
3552        });
3553}
3554function createSalesQuoteAsSalesPerson(customerNo, email) {
3555    if (email != null && email != "" && email.length > 0) {
3556        email = "/" + email;
3557    }
3558
3559    $.ajax({
3560        method: "GET",
3561        url: "/shop/api/SalesPersonCreateQuote/" + customerNo + email,
3562        cache: false,
3563    })
3564        .fail(function (data) {
3565            alert(data.responseText);
3566        })
3567        .done(function (data) {
3568            location.href = "/shop/api/SalesPersonImpersonate/" + customerNo + "/" + data;
3569        });
3570}
3571function deleteSalesQuote(id) {
3572    $("body").addClass("loading");
3573    $.ajax({
3574        method: "GET",
3575        url: "/api/basket/deleteselectedsalesquote?id=" + id,
3576        cache: false,
3577        dataType: "json",
3578        contentType: "application/json",
3579    })
3580        .done(function () {})
3581        .always(function () {
3582            window.location.href = "/basket";
3583        });
3584}
3585function renameSalesQuote() {
3586    let url =
3587        "/api/basket/renamesalesquote?quoteno=" +
3588        $("#selectUserSalesQuote").attr("selectedNo") +
3589        "&id=" +
3590        $("#selectUserSalesQuote").attr("selectedId") +
3591        "&description=" +
3592        encodeURIComponent($("#salesQuoteDescription").val());
3593    $("body").addClass("loading");
3594
3595    $.ajax({
3596        method: "POST",
3597        url: url,
3598        cache: false,
3599        dataType: "json",
3600        contentType: "application/json",
3601    })
3602        .done(function () {})
3603        .always(function () {
3604            window.location.href = url;
3605        });
3606}
3607
3608// --- CHECKOUT --- //
3609function completeOrder(elem_) {
3610    let object = {};
3611
3612    if (!isOrderReadyForCompletion(elem_)) {
3613        return;
3614    }
3615
3616    $(".checkoutDiv input").each(function () {
3617        object[$(this).attr("name")] = $(this).val();
3618    });
3619
3620    if ($("#addShippingAddress")[0] != null) {
3621        object["newShippingAddress"] = $("#addShippingAddress")[0].checked;
3622    }
3623    if ($("#shippingAddressSelector") != null) {
3624        object["shippingAddressSelector"] = $("#shippingAddressSelector").val();
3625    }
3626
3627    if ($("[name=shipToCountry]") != null) {
3628        object["shipToCountry"] = $("[name=shipToCountry]").find(":selected").val();
3629    }
3630
3631    if ($("[name=shipToSaveAddress]") != null) {
3632        object["shipToSaveAddress"] = $("[name=shipToSaveAddress]").is(":checked") ? "true" : "";
3633    }
3634
3635    let data_ = JSON.stringify(object);
3636
3637    $("body").addClass("loading");
3638
3639    $.ajax({
3640        url: "/api/basket/edit/complete/",
3641        method: "POST",
3642        contentType: "application/json",
3643        data: data_,
3644        cache: false,
3645    })
3646        .fail(function (response) {
3647            $("body").removeClass("loading");
3648            $(elem_).removeClass("loading").addClass("error").attr("last-class", "error");
3649            let errorOverlay = $("<div>").addClass("overlay errorOverlay").html(response.responseText);
3650            $(errorOverlay)
3651                .find(":after")
3652                .on("click", function () {
3653                    $(errorOverlay).remove();
3654                });
3655            $(elem_).after($(errorOverlay));
3656            setTimeout(function () {
3657                $(elem_).removeClass("error");
3658            }, 3000);
3659        })
3660        .done(function (response) {
3661            $("body").removeClass("loading");
3662
3663            $(".checkoutDiv").hide();
3664
3665            if (response != undefined && response != "" && response != null) {
3666                $("#orderNo").html(response);
3667                $(".orderCompleted").removeClass("d-none");
3668                $(".dynBasketQuantity").text("0");
3669                onOrderCompleted();
3670            }
3671        });
3672}
3673function isOrderReadyForCompletion(elem_) {
3674    $(elem_).addClass("loading");
3675
3676    let anyErrors = false;
3677    let errorList = [];
3678
3679    const checkRequiredFields = () => {
3680        if (requiredShippingFieldsFilled()) {
3681            return false;
3682        }
3683        errorList.push($(errorTexts).attr("fillInNecessaryFields"));
3684        return true;
3685    };
3686
3687    anyErrors = checkRequiredFields();
3688
3689    if (anyErrors) {
3690        $(elem_).removeClass("loading").addClass("error").attr("last-class", "error");
3691        setTimeout(function () {
3692            let errorOverlay = $("<div>").addClass("overlay errorOverlay").html(errorList.join("<br>"));
3693            $(errorOverlay)
3694                .find(":after")
3695                .on("click", function () {
3696                    $(errorOverlay).remove();
3697                });
3698            $(elem_).after($(errorOverlay));
3699        }, 100);
3700        setTimeout(function () {
3701            $(elem_).removeClass("error");
3702        }, 3000);
3703        return false;
3704    }
3705    return true;
3706}
3707function saveShipmentMethod(elem_) {
3708    let sq = $(elem_).data("salesquote");
3709    let code = $(elem_).find(":selected").val();
3710    $("body").addClass("loading");
3711
3712    if (sq != null && sq != "" && code != null) {
3713        $("#completeOrder").prop("disabled", true);
3714        $("#completeOrder").text(WaitMessage);
3715
3716        $.ajax({
3717            method: "POST",
3718            url: "/api/basket/updateshipmentmethod/" + sq + "/" + code,
3719            cache: false,
3720            dataType: "json",
3721            contentType: "application/json",
3722        })
3723            .fail(function (jqXHR) {})
3724            .always(function () {
3725                $("#completeOrder").prop("disabled", false);
3726                $("#completeOrder").text(CompleteMessage);
3727                $("body").removeClass("loading");
3728            });
3729    }
3730}
3731function saveShippingAgent(elem_) {
3732    let sq = $(elem_).data("salesquote");
3733    let code = $(elem_).find(":selected").val();
3734    $("body").addClass("loading");
3735    $("#completeOrder").prop("disabled", true);
3736    $("#completeOrder").text(WaitMessage);
3737
3738    $.ajax({
3739        method: "POST",
3740        url: "/api/basket/updateshippingagent/" + sq + "/" + encodeURIComponent(code),
3741        cache: false,
3742        contentType: "application/json",
3743    })
3744        .fail(function (jqXHR) {})
3745        .always(function () {
3746            document.location.href = document.location.href.split("#")[0];
3747        });
3748}
3749function saveShippingAgentService(elem_) {
3750    let sq = $(elem_).data("salesquote");
3751    let code = $(elem_).find(":selected").val();
3752    $("body").addClass("loading");
3753    $("#completeOrder").prop("disabled", true);
3754    $("#completeOrder").text(WaitMessage);
3755
3756    $.ajax({
3757        method: "POST",
3758        url: "/api/basket/updateshippingagentservice/" + sq + "/" + encodeURIComponent(code),
3759        cache: false,
3760        contentType: "application/json",
3761    })
3762        .fail(function (jqXHR) {})
3763        .always(function () {
3764            document.location.href = document.location.href.split("#")[0];
3765        });
3766}
3767function saveHeaderExtDocNo(elem_) {
3768    $("#completeOrder").prop("disabled", true);
3769    $("#completeOrder").text(WaitMessage);
3770    $("body").addClass("loading");
3771
3772    $.ajax({
3773        method: "POST",
3774        url: "/api/basket/edit/externaldocumentno/" + encodeURIComponent($(elem_).val()),
3775        cache: false,
3776        contentType: "application/json",
3777    })
3778        .done(function (data) {})
3779        .fail(function (jqXHR, textStatus) {})
3780        .always(function (data) {
3781            if (data == "1") {
3782                document.location.href = document.location.href.split("#")[0];
3783            } else {
3784                $("body").removeClass("loading");
3785                $("#completeOrder").prop("disabled", false);
3786                $("#completeOrder").text(CompleteMessage);
3787            }
3788        });
3789}
3790function saveHeaderYourRef(elm, sqNo) {
3791    var oldbtnText = $("#completeOrder").text();
3792
3793    $("#completeOrder").prop("disabled", true);
3794    $("#completeOrder").text(WaitMessage);
3795    $("body").addClass("loading");
3796
3797    postData = '{"YourReference":"' + $(elm).val() + '"}';
3798
3799    $.ajax({
3800        method: "POST",
3801        url: "/api/basket/edit/salesorderinfo/" + sqNo,
3802        cache: false,
3803        data: postData,
3804        contentType: "application/json",
3805    })
3806        .done(function (data) {})
3807        .fail(function (jqXHR, textStatus) {})
3808        .always(function (data) {
3809            if (data == "1") {
3810                document.location.href = document.location.href.split("#")[0];
3811            } else {
3812                $("body").removeClass("loading");
3813                $("#completeOrder").prop("disabled", false);
3814                $("#completeOrder").text(CompleteMessage);
3815                $("#completeOrder").text(oldbtnText);
3816            }
3817        });
3818}
3819function updateSellToContact(elem_) {
3820    var contactNo = $(elem_).val();
3821    if (!contactNo) return;
3822
3823    $("body").addClass("loading");
3824
3825    $.ajax({
3826        method: "POST",
3827        url: "/api/basket/edit/selltocontactno/" + encodeURIComponent(contactNo),
3828        cache: false,
3829        contentType: "application/json",
3830    })
3831        .done(function () {
3832            document.location.href = document.location.href.split("#")[0];
3833        })
3834        .fail(function () {
3835            $("body").removeClass("loading");
3836        });
3837}
3838function UpdateRequestedDeliveryDate(elm) {
3839    $("#completeOrder").prop("disabled", true);
3840    $("#completeOrder").text(WaitMessage);
3841    $("body").addClass("loading");
3842    $.ajax({
3843        method: "POST",
3844        url: "/api/basket/updatedeliverydate/" + $(elm).val(),
3845        cache: false,
3846        dataType: "json",
3847        contentType: "application/json",
3848    })
3849        .done(function (data) {})
3850        .fail(function (jqXHR, textStatus) {})
3851        .always(function () {
3852            document.location.href = document.location.href.split("#")[0];
3853        });
3854}
3855function requiredShippingFieldsFilled() {
3856    let allFilled = true;
3857    $(".orderInfoRow.required").each(function () {
3858        let input = $(this).find("input, select");
3859        input.removeClass("form-error");
3860        input.each(function () {
3861            if (
3862                ($(this).is("input") && ($(this).val() == undefined || $($(this)).val() == null || $($(this)).val() == "")) ||
3863                ($(this).is("select") && $(this).find("option:selected[null]").length)
3864            ) {
3865                if ($(this).parents(".optional.unrequire").length) {
3866                    return;
3867                }
3868                $(this).addClass("form-error");
3869                allFilled = false;
3870            }
3871        });
3872    });
3873    return allFilled;
3874}
3875
3876// Filter shipping addresses in the dropdown based on search input
3877function filterShippingAddresses(searchInput) {
3878    let searchTerm = searchInput.value.toLowerCase().trim();
3879    let select = document.getElementById("shippingAddressSelector");
3880
3881    if (!select) return;
3882
3883    let options = select.options;
3884    let firstVisibleIndex = -1;
3885    let visibleCount = 0;
3886    let isSearching = searchTerm !== "";
3887
3888    for (let i = 0; i < options.length; i++) {
3889        let option = options[i];
3890        let optionText = option.text.toLowerCase();
3891        let optionValue = option.value;
3892
3893        // Hide placeholder options when searching, show when not searching
3894        if (optionValue === "" || optionValue === "-1") {
3895            if (isSearching) {
3896                // Hide placeholder options while searching
3897                option.style.display = "none";
3898                option.hidden = true;
3899            } else {
3900                // Show placeholder options when not searching
3901                option.style.display = "";
3902                option.hidden = false;
3903                if (firstVisibleIndex === -1) firstVisibleIndex = i;
3904                visibleCount++;
3905            }
3906            continue;
3907        }
3908
3909        // Filter based on search term
3910        if (!isSearching || optionText.includes(searchTerm)) {
3911            option.style.display = "";
3912            option.hidden = false;
3913            if (firstVisibleIndex === -1) firstVisibleIndex = i;
3914            visibleCount++;
3915        } else {
3916            option.style.display = "none";
3917            option.hidden = true;
3918        }
3919    }
3920
3921    // Expand select to show options when searching, collapse when search is cleared
3922    if (isSearching) {
3923        // Show up to 8 visible options (or less if fewer match)
3924        select.size = Math.min(visibleCount, 8);
3925        select.classList.add("shipping-address-expanded");
3926    } else {
3927        // Collapse back to normal dropdown
3928        select.size = 0;
3929        select.removeAttribute("size");
3930        select.classList.remove("shipping-address-expanded");
3931    }
3932
3933    // If current selection is hidden, reset to first visible option
3934    if (select.selectedIndex >= 0 && options[select.selectedIndex].hidden) {
3935        select.selectedIndex = firstVisibleIndex >= 0 ? firstVisibleIndex : 0;
3936    }
3937}
3938
3939function shippingAddressSelectorChange(salesquoteId, _elm) {
3940    // Skip if we're just navigating with keyboard (not actually selecting)
3941    let select = document.getElementById("shippingAddressSelector");
3942    if (select) {
3943        select.size = 0;
3944        select.removeAttribute("size");
3945        select.classList.remove("shipping-address-expanded");
3946        // Reset all options to visible
3947        for (let i = 0; i < select.options.length; i++) {
3948            select.options[i].style.display = "";
3949            select.options[i].hidden = false;
3950        }
3951    }
3952
3953    setOrDeleteCookie(salesquoteId, _elm);
3954    if ($(_elm).attr("type") == "checkbox") {
3955        if ($(_elm).is(":checked")) {
3956            $(".newShippingAddress").show().removeClass("unrequire");
3957        } else {
3958            $(".newShippingAddress").hide().addClass("unrequire");
3959        }
3960        return;
3961    }
3962    let selval = $("#shippingAddressSelector option:selected").val();
3963    //None selected
3964    if (selval == "") {
3965        $(".newShippingAddress").hide();
3966        $(".existingShippingAddress").hide();
3967
3968        $("body").addClass("loading");
3969
3970        $.ajax({
3971            method: "POST",
3972            url: "/api/basket/updateshipto/",
3973            cache: false,
3974            dataType: "json",
3975            contentType: "application/json",
3976        })
3977            .done(function (data) {})
3978            .always(function (data) {
3979                if (data.responseText == "reload") {
3980                    document.location.href = document.location.href.split("#")[0];
3981                } else {
3982                    $("body").removeClass("loading");
3983                }
3984            });
3985
3986        $(".newShippingAddress :input").attr("disabled", false);
3987        $(".newShippingAddress").hide();
3988        $(".existingShippingAddress").hide();
3989    } else if (selval == "-1") {
3990        //Write new address
3991        $("body").addClass("loading");
3992
3993        $.ajax({
3994            method: "POST",
3995            url: "/api/basket/updateshipto/",
3996            cache: false,
3997            dataType: "json",
3998            contentType: "application/json",
3999        }).always(function (data) {
4000            if (data.responseText == "reload") {
4001                document.location.href = document.location.href.split("#")[0];
4002            } else {
4003                $("body").removeClass("loading");
4004            }
4005        });
4006
4007        $(".newShippingAddress :input").attr("disabled", false);
4008        $(".newShippingAddress").show();
4009        $(".existingShippingAddress").hide();
4010    }
4011    //Select address
4012    else {
4013        $(".newShippingAddress").hide();
4014        $("body").addClass("loading");
4015
4016        $.ajax({
4017            method: "POST",
4018            url: "/api/basket/updateshipto?shiptocode=" + encodeURIComponent(selval),
4019            cache: false,
4020            dataType: "json",
4021            contentType: "application/json",
4022        })
4023            .fail(function (jqXHR) {})
4024            .always(function (data) {
4025                if (data.responseText == "reload") {
4026                    document.location.href = document.location.href.split("#")[0];
4027                } else {
4028                    $("body").removeClass("loading");
4029
4030                    $.ajax({
4031                        method: "GET",
4032                        url: "/shop/api/GetCustomerShippingAddressList?selectedCode=" + encodeURIComponent(selval),
4033                        cache: false,
4034                    }).done(function (data) {
4035                        $('input[name="existingShipToName"]').val(data[0].name);
4036                        $('input[name="existingShipToContact"]').val(data[0].contact);
4037                        $('input[name="existingShipToAddressLine1"]').val(data[0].address);
4038                        $('input[name="existingShipToAddressLine2"]').val(data[0].address2);
4039                        $('input[name="existingShipToPostCode"]').val(data[0].zipcode);
4040                        $('input[name="existingShipToCity"]').val(data[0].city);
4041                        $('input[name="existingShipToCountry"]').val(data[0].country);
4042                        $(".existingShippingAddress").show();
4043                    });
4044                }
4045            });
4046    }
4047}
4048
4049// --- SALESPERSON --- //
4050function resetSalesPersonLogin() {
4051    deleteCookie("_scSPID");
4052    deleteCookie("_scID");
4053    deleteCookie("_scIDV2");
4054    location.reload();
4055}
4056function isoDateReviver(value) {
4057    if (typeof value === "string") {
4058        var a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)(?:([\+-])(\d{2})\:(\d{2}))?Z?$/.exec(value);
4059        if (a) {
4060            var utcMilliseconds = Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]);
4061            return new Date(utcMilliseconds);
4062        }
4063    }
4064    return value;
4065}
4066function closeQuoteDrawer() {
4067    $("body").removeClass("quoteActive");
4068    $("tr.active").removeClass("active");
4069    $('.drawer-salesquotes [class^="contentSection"]').html("");
4070}
4071function updatePage(search = false, bottom = false) {
4072    $(".salesCustomerList").addClass("loading");
4073    visibleCustomersList = $(".salesCustomerList tbody tr:not(.hidden)");
4074    customerCount = visibleCustomersList.length;
4075    if (!totalCustomerPages) {
4076        totalCustomerPages = Math.ceil(customerCount / customersPerPage);
4077    }
4078    if (search) {
4079        totalCustomerPages = Math.ceil(customerCount / Math.max(customersPerPage, 100));
4080        currentCustomerPage = 1;
4081    }
4082
4083    $(".pageSelect :is(.first-page, .prev-page)").attr("disabled", currentCustomerPage == 1);
4084    $(".pageSelect :is(.last-page, .next-page)").attr("disabled", currentCustomerPage == totalCustomerPages);
4085
4086    $(".pageSelect").find("input").val(currentCustomerPage).attr("max", totalCustomerPages);
4087    $(".pageSelect")
4088        .find("span")
4089        .html("/" + totalCustomerPages);
4090    $(".pageSelect")
4091        .find("span")
4092        .html("/" + totalCustomerPages);
4093    if (bottom) {
4094        $(".customersContainer").animate(
4095            {
4096                scrollTop: 0,
4097            },
4098            300,
4099            "myEasing",
4100        );
4101    }
4102    debounce(calculateHidden(), 300);
4103}
4104function calculateHidden() {
4105    let counter = (currentCustomerPage - 1) * customersPerPage;
4106    let newPage = [];
4107    let chunkSize = 5;
4108    function partialWhile() {
4109        let chunkLimit = Math.min(counter + chunkSize, currentCustomerPage * customersPerPage);
4110        while (counter < chunkLimit) {
4111            newPage.push(visibleCustomersList[counter]);
4112            counter++;
4113        }
4114        if (chunkLimit == currentCustomerPage * customersPerPage) {
4115            $(".inPage").removeClass("inPage");
4116            $(newPage).addClass("inPage");
4117            $(".salesCustomerList").removeClass("loading");
4118            return;
4119        }
4120        requestAnimationFrame(partialWhile);
4121    }
4122    requestAnimationFrame(partialWhile);
4123}
4124function showDrawer(elm) {
4125    let drawer = $(".drawer-salesquotes");
4126    let customerNo = $(elm).data("customer");
4127    let customerName = $(elm).find(".customerName span").html();
4128    $("tr.active").removeClass("active");
4129    $(elm).addClass("active");
4130
4131    $(".drawer-salesquotes").addClass("loading");
4132    $(drawer).find('.header, [class^="contentSection"]').html("");
4133
4134    $.ajax({
4135        method: "GET",
4136        url: "/shop/api/SalesPersonCustomerQuotesV2/" + customerNo,
4137        cache: false,
4138    }).done(function (data) {
4139        showSalesQuotes(data, drawer, customerNo, customerName);
4140    });
4141
4142    if ($(".tab-control").length) {
4143        let quoteCount = $(elm).find(".quoteCount").text();
4144        $("#dynQuotesAmt").text("(" + quoteCount + ")");
4145        let orderCount = $(elm).find(".orderCount").text();
4146        $("#dynOrdersAmt").text("(" + orderCount + ")");
4147        if (!$(".tab-control .active").length) {
4148            $(".quotesTab").addClass("active");
4149        }
4150
4151        $.ajax({
4152            method: "GET",
4153            url: "/shop/api/SalesPersonCustomerOrders/" + customerNo,
4154            cache: false,
4155        }).done(function (data) {
4156            showSalesOrders(data, drawer);
4157        });
4158    }
4159}
4160function showSalesQuotes(data, drawer, customerNo, customerName) {
4161    $(drawer).find(".header").html(customerName);
4162    let contentSection = $(drawer).find(".contentSectionQuotes");
4163    $(data).each(function () {
4164        $(drawer).removeClass("loading");
4165        let userName = this.name;
4166        if (userName == "Your quotes") {
4167            $(contentSection).append($("<h6>").html(salesperson_label_you).addClass("divider"));
4168        } else if ($(contentSection).find(".linesSection").length == 1) {
4169            $(contentSection).append($("<h6>").html(salesperson_label_users).addClass("divider"));
4170        }
4171        let userEmail = this.eCommCartUserEmail;
4172        //USER SECTION
4173        let userSection = $("<div>").addClass("linesSection").attr("user-email", userEmail);
4174        // header
4175        let userHeader = $("<div>").addClass("sectionHeader");
4176        let userHeaderName = $("<h6>").html(userEmail);
4177
4178        userHeader.append(userHeaderName);
4179        if (this.canCreateSalesQuote) {
4180            let createQuote = $("<button>")
4181                .addClass("btn btn-sm btn-secondary")
4182                .html($(".CreateQuoteTxt").html())
4183                .on("click", function () {
4184                    createSalesQuoteAsSalesPerson(customerNo, userEmail);
4185                });
4186            userHeader.append(createQuote);
4187        }
4188        //body
4189        let userBody = $("<div>").addClass("linesBody");
4190        let quotes = this.userSalesQuoteDataItems;
4191        $(quotes).each(function () {
4192            let quote = $("<div>")
4193                .addClass("salesQuote")
4194                .attr("data-href", "/shop/api/SalesPersonImpersonate/" + customerNo + "/" + this.quoteId);
4195
4196            let quoteDate = $("<span>").html(this.description).addClass("quoteDate");
4197            let quoteNo = $("<span>").html(this.quoteNo).addClass("quoteNo");
4198            let quoteLines = $("<span>").html(this.lineCount).addClass("quoteLines");
4199            let quoteTotal = $("<span>").html(this.totalInclTax).addClass("quoteTotal");
4200
4201            quote.append(quoteDate, quoteNo, quoteLines, quoteTotal);
4202            userBody.append(quote);
4203        });
4204        userSection.append(userHeader, userBody);
4205        $(contentSection).append(userSection);
4206    });
4207}
4208function showSalesOrders(data, drawer) {
4209    let ordersHeader = $("<div>").addClass("ordersHeader").append($("<h6>").html(salesperson_label_Open_Sales_Orders).addClass("divider"));
4210    let ordersBody = $("<div>").addClass("linesBody");
4211    $(data).each(function () {
4212        let order = $("<div>").addClass("salesOrder");
4213        let od = new Date(this.orderDate);
4214        let rdd = new Date(this.requestedDeliveryDate);
4215        let now = new Date();
4216        let isLate = false;
4217        let hasRdd = od < rdd;
4218        if (hasRdd) {
4219            if (rdd < now) {
4220                isLate = true;
4221            }
4222        }
4223        let rddClass = isLate ? " isLate" : "";
4224        rddClass += hasRdd ? "" : " noRDD";
4225        let orderDate = $("<span>").html(od.toLocaleDateString()).addClass("orderDate");
4226        let orderRDD = $("<span>")
4227            .html(hasRdd ? rdd.toLocaleDateString() : "")
4228            .addClass("orderRDD" + rddClass)
4229            .attr("title", "Requested Delivery Date");
4230        let orderNo = $("<span>").html(this.number).addClass("orderNo");
4231        let orderTotal = $("<span>")
4232            .html(this.totalAmountIncludingTax + " " + this.currencyCode)
4233            .addClass("orderTotal");
4234
4235        order.append(orderDate, orderNo, orderRDD, orderTotal);
4236        ordersBody.append(order);
4237    });
4238    let ordersSection = $("<div>").addClass("linesSection").append(ordersBody);
4239    $(drawer).find(".contentSectionOrders").append(ordersHeader, ordersSection);
4240}
4241function customerSearch() {
4242    const form = $(".salesCustomerSearch");
4243    const value = $(form).val();
4244    $(".salesCustomerList tr").each(function (index) {
4245        if (index !== 0) {
4246            const $row = $(this);
4247            var matchedIndex = $row.find("td").text().toLowerCase().indexOf(value.toLowerCase());
4248            if (matchedIndex == -1) {
4249                $row.addClass("hidden");
4250            } else {
4251                $row.removeClass("hidden");
4252            }
4253        }
4254    });
4255    updatePage(true);
4256}
4257function cancelSearch() {
4258    if ($("#search").val() == "") return;
4259    $("#search").val("").trigger("input");
4260}
4261
4262// --- FAVORITES PAGES --- //
4263function showFavsListForm(btn) {
4264    let newFavList = $(btn).siblings(".newFavList");
4265    $(newFavList).removeClass("d-none").show();
4266    $(btn).hide();
4267    newFavList
4268        .find("#newFavoriteListName")
4269        .focus()
4270        .on("keydown", function (e) {
4271            favsListCreate;
4272            if (e.keyCode == 13) {
4273                $("#favsListCreate").trigger("click");
4274            }
4275        });
4276    newFavList
4277        .find("#favsListCancel")
4278        .off()
4279        .on("click", function () {
4280            newFavList.hide();
4281            $(btn).show();
4282        });
4283    newFavList
4284        .find("#favsListCreate")
4285        .off()
4286        .on("click", function () {
4287            createFavoriteList($(this));
4288        });
4289}
4290function createFavoriteList(btn) {
4291    var listName = $("#newFavoriteListName").val();
4292    $(btn).addClass("loading");
4293    $("body").addClass("loading");
4294
4295    $.post("/favorites/add", {
4296        listName: listName,
4297    })
4298        .done(function () {
4299            $(btn).addClass("success").attr("last-class", "success");
4300            location.reload();
4301        })
4302        .fail(function (xhr) {
4303            btn.addClass("error").attr("last-class", "error");
4304        })
4305        .always(function () {
4306            $(btn).removeClass("loading");
4307            $("body").removeClass("loading");
4308            setTimeout(function () {
4309                $(btn).removeClass("success error");
4310            }, 3000);
4311        });
4312}
4313function renameFavoriteList(btn, favoriteCode, headerId) {
4314    let name = $(btn).parent();
4315    if (!$(name).hasClass("editing")) {
4316        $(".name.editing").removeClass("editing");
4317        $(name).addClass("editing");
4318        let input = $(name).find("input");
4319        let newVal = $(name).find(".favoriteName").html();
4320        input.val(newVal);
4321        input.select();
4322        input.off().on("keydown", function (e) {
4323            if (e.keyCode == 13) {
4324                renameFavoriteList(btn, favoriteCode, headerId);
4325            }
4326        });
4327        return;
4328    }
4329
4330    let newListName = $(name).find(".favoriteNameEdit").val();
4331    $(btn).addClass("loading");
4332    $.post("/favorites/edit", {
4333        favoriteCode: favoriteCode,
4334        newListName: newListName,
4335        headerId: headerId,
4336    })
4337        .done(function () {
4338            $(btn).siblings(".favoriteName").html(newListName);
4339            $(btn).removeClass("loading").addClass("success");
4340            $(".name.editing").removeClass("editing");
4341        })
4342        .fail(function (xhr) {
4343            $(btn).removeClass("loading").addClass("error");
4344            alert("Failed to update favorite list name: " + xhr.responseJSON.message);
4345        })
4346        .always(function () {
4347            setTimeout(function () {
4348                $(btn).removeClass("success error");
4349            }, 3000);
4350        });
4351}
4352function cancelRename(btn) {
4353    let name = $(btn).parent();
4354    $(name).removeClass("editing");
4355}
4356function editFavoriteList(button) {
4357    var row = button.closest("tr");
4358    row.querySelector(".favoriteName").classList.add("d-none");
4359    row.querySelector(".favoriteNameEdit").classList.remove("d-none");
4360    button.classList.add("d-none");
4361    row.querySelector(".btn-primary").classList.remove("d-none");
4362}
4363function toggleConfirm(btn, show = false) {
4364    let initDelete = $(btn).parent();
4365    if (!show) {
4366        initDelete.removeClass("show");
4367        return;
4368    }
4369    if (!initDelete.hasClass("show")) {
4370        initDelete.addClass("show");
4371        return;
4372    }
4373    deletetFavoriteList(initDelete.data("headerid"), btn);
4374}
4375function deletetFavoriteList(headerId, btn) {
4376    $(btn).addClass("loading");
4377    let thisRow = $(btn).closest("tr");
4378    thisRow.css("max-height", thisRow.outerHeight());
4379    $.post("/favorites/removeList", {
4380        headerId: headerId,
4381    })
4382        .done(function () {
4383            $(thisRow).addClass("deleting");
4384            setTimeout(function () {
4385                $(thisRow).remove();
4386            }, 500);
4387        })
4388        .fail(function (xhr) {
4389            $(btn).removeClass("loading").addClass("error");
4390            setTimeout(function () {
4391                $(btn).removeClass("error");
4392            }, 3000);
4393            alert("Failed to remove favorite list: " + xhr.responseJSON);
4394        });
4395}
4396
4397// --- COMPARE ITEMS --- //
4398function compareScroll(btn, directionForward = true) {
4399    if ($(btn).attr("disabled")) return;
4400    const containerW = $(".tableContainer").outerWidth();
4401    const itemW = $('.compareTable thead th[scope="col"]').first().outerWidth();
4402    let scrollLeft = $(".tableContainer").scrollLeft();
4403
4404    if ($(".compareTable").hasClass("sideScroll") || $('body:not([class*="--m"])').length) {
4405        const gutter = Number($(".compareTable tr").first().css("column-gap").replace("px", ""));
4406        const multiplier = directionForward ? 1 : -1;
4407        const toScroll = (itemW + gutter) * multiplier;
4408
4409        scrollLeft += toScroll;
4410
4411        $(".tableContainer").animate(
4412            {
4413                scrollLeft: scrollLeft,
4414            },
4415            200,
4416        );
4417    }
4418    $(".compareTable:not(.sideScroll)").addClass("sideScroll");
4419
4420    setTimeout(() => {
4421        const limitLeft = scrollLeft <= 4;
4422        const limitRight = scrollLeft + containerW >= $(".compareTable").width() - itemW / 10;
4423        $(".tableControls > div").removeAttr("disabled");
4424        if (limitLeft) {
4425            $(".tableControls .left").attr("disabled", "disabled");
4426        }
4427        if (limitRight) {
4428            $(".tableControls .right").attr("disabled", "disabled");
4429        }
4430    }, 200);
4431}
4432function addToCompareList(e, btn, productNo) {
4433    e.preventDefault();
4434    if ($(btn).find(".fas").hasClass("fa-check")) {
4435        $.ajax({
4436            method: "POST",
4437            url: "/api/itemcompareremove/" + productNo,
4438            cache: false,
4439        })
4440            .done(function () {
4441                decrementQuantity();
4442            })
4443            .always(function () {
4444                $(btn).find(".fas").removeClass("fa-check").addClass("fa-right-left");
4445            });
4446        return;
4447    }
4448    $.ajax({
4449        method: "POST",
4450        url: "/api/itemcompareadd/" + productNo,
4451        cache: false,
4452    })
4453        .done(function () {
4454            incrementQuantity();
4455        })
4456        .always(function () {
4457            $(btn).find(".fas").removeClass("fa-right-left").addClass("fa-check");
4458        });
4459}
4460function removeFromCompareList(elm) {
4461    let $elm = $(elm);
4462    let index = $elm.parents("th").data("index");
4463    $(".deleting").each(function () {
4464        $(this).parent().addClass("noTransitions");
4465        $(this).remove();
4466    });
4467
4468    setTimeout(function () {
4469        $(".compareTable  tr").each(function () {
4470            $(this).removeClass("noTransitions");
4471            $($(this).find(`:is(td, th)[data-index="${index}"]`)).addClass("deleting");
4472        });
4473        productNo = $elm.parent().data("productno");
4474        $.ajax({
4475            method: "POST",
4476            url: "/api/itemcompareremove/" + productNo,
4477            cache: false,
4478        })
4479            .done(function () {
4480                decrementQuantity();
4481            })
4482            .fail(function (jqXHR, response) {});
4483    }, 100);
4484}
4485function incrementQuantity() {
4486    const quantityElement = document.querySelector(".dynCompareQuantity");
4487
4488    if (quantityElement) {
4489        const currentValue = parseInt(quantityElement.textContent.trim(), 10);
4490
4491        if (!isNaN(currentValue)) {
4492            quantityElement.textContent = currentValue + 1;
4493        } else {
4494            console.error("The content is not a valid number.");
4495        }
4496    } else {
4497        console.error('Element with class "dynCompareQuantity" not found.');
4498    }
4499}
4500function decrementQuantity() {
4501    const quantityElement = document.querySelector(".dynCompareQuantity");
4502
4503    if (quantityElement) {
4504        const currentValue = parseInt(quantityElement.textContent.trim(), 10);
4505
4506        if (!isNaN(currentValue)) {
4507            quantityElement.textContent = currentValue - 1;
4508        } else {
4509            console.error("The content is not a valid number.");
4510        }
4511    } else {
4512        console.error('Element with class "dynCompareQuantity" not found.');
4513    }
4514}
4515function itemCompareOverflow() {
4516    $(".tableContainer").addClass("noTransitions");
4517    setTimeout(function () {
4518        $(".tableContainer").removeClass("noTransitions");
4519    }, 500);
4520    if ($(".compareTable").width() <= $(".tableContainer").width()) {
4521        $(".compareSection").removeClass("overflow");
4522        $(".compareTable").removeClass("sideScroll");
4523        return;
4524    }
4525    $(".compareSection").addClass("overflow");
4526    $(".tableContainer").scrollLeft(0);
4527    $(".tableControls > div").removeAttr("disabled");
4528}
4529
4530// --- DOM MANIPULATION --- //
4531function appendLogos() {
4532    let dir = $(".logoSection").find("img").attr("src");
4533    let lastIndex = dir.lastIndexOf("/");
4534    let logoSection = $(".logoSection");
4535    let logoTemplate = $(logoSection).find("img");
4536    $(logoSection).html("");
4537    for (let i = 0; i < multiLogos.length; i++) {
4538        let newDir = dir.substring(0, lastIndex) + "/" + multiLogos[i];
4539        let newLogo = $(logoTemplate).clone().attr("src", newDir);
4540        $(logoSection).append($(newLogo));
4541    }
4542}
4543function resizeNavbar(scrollTop) {
4544    if (scrollTop) $("body:not(.miniNav)").addClass("miniNav");
4545    else $(".miniNav").removeClass("miniNav");
4546}
4547function showVideo(htmldata) {
4548    $(".videoplayer").html(htmldata);
4549    $(".videoplayer").show();
4550    $(".imagediv").hide();
4551}
4552function reElastic(elm) {
4553    elm.show().css("height", "unset");
4554    let paddingTop = parseInt(elm.css("padding-top"));
4555    let paddingBottom = parseInt(elm.css("padding-bottom"));
4556    let scrollHeight = parseInt(elm.prop("scrollHeight"));
4557    elm.css("height", paddingTop + paddingBottom + scrollHeight + "px");
4558    $("body.loading").removeClass("loading");
4559}
4560function checkWidthDependantElements() {
4561    setWidthClass();
4562    if (sideFilter) {
4563        let collapseSideFilter = collapseMainNav;
4564        if (sideFilterCollapseWidth) collapseSideFilter = parseFloat($("body").width()) < sideFilterCollapseWidth;
4565        if (collapseSideFilter && !$("body").hasClass("sideFilterCollapse")) $("body").addClass("sideFilterCollapse");
4566        else if (!collapseSideFilter && $("body").hasClass("sideFilterCollapse")) $("body").removeClass("sideFilterCollapse");
4567    }
4568    if (!sidebarEnabled) {
4569        if (!collapseMainNav) {
4570            logoSectionWidth = $(".logoSection").outerWidth();
4571            extendedMainNavWidth = $("#main_nav").outerWidth();
4572        }
4573        collapseMainNav =
4574            screenWidth < navCollapseWidth ||
4575            (navCollapseWidth == 0 && $(".navbar > .container").width() - logoSectionWidth < extendedMainNavWidth);
4576    } else collapseMainNav = screenWidth < sidebarCollapseWidth;
4577    if (autoCollapseNav && collapseMainNav) $("body:not(.collapseMainNav)").addClass("collapseMainNav").removeClass("displayMainNav");
4578    else {
4579        $("body:not(.displayMainNav)").addClass("displayMainNav").removeClass("collapseMainNav");
4580    }
4581    if (page == "itemcompare") {
4582        itemCompareOverflow();
4583    }
4584    $("body").addClass("bodyReady");
4585}
4586function calculateScrollbarWidth() {
4587    const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
4588    $(":root").css("--scrollbarWidth", scrollbarWidth + "px");
4589}
4590function setWidthClass() {
4591    let widthClass = "screenW--xs";
4592    if (screenWidth >= 460) widthClass += "--s";
4593    if (screenWidth >= 720) widthClass += "--m";
4594    if (screenWidth >= 1024) widthClass += "--l";
4595    if (screenWidth >= 1366) widthClass += "--xl";
4596    if (screenWidth >= 1600) widthClass += "--xxl";
4597    let prevClass = $("body")
4598        .attr("class")
4599        .split(" ")
4600        .find((x) => x.includes("screenW"));
4601    if (prevClass) {
4602        if (prevClass == widthClass) return;
4603        $("body").removeClass(prevClass);
4604    }
4605    $("body").addClass(widthClass);
4606}
4607function changeGalleryClass(btn) {
4608    currentGalleryClass = $(btn).attr("value");
4609    $(".GalleryBox").attr("class", "GalleryBox");
4610    setUrlParams();
4611}
4612function changeAltDelivery() {
4613    if ($("#AltDelivery").is(":checked")) {
4614        $("#AltDeliveryDiv input, #AltDeliveryDiv textarea, #AltDeliveryDiv select").removeAttr("disabeld");
4615        $("#AltDeliveryDiv").show();
4616    } else {
4617        $("#AltDeliveryDiv input, #AltDeliveryDiv textarea, #AltDeliveryDiv select").attr("disabeld", "disabeld");
4618        $("#AltDeliveryDiv").hide();
4619    }
4620}
4621// menus
4622function MakeMenuStructure(callback = null) {
4623    $.ajax({
4624        url: "/api/categories",
4625        method: "GET",
4626        contentType: "application/json",
4627        cache: false,
4628    })
4629        .fail(function () {
4630            console.error("Failed to load master data");
4631        })
4632        .done(function (response) {
4633            let html_ = "";
4634
4635            if (manyCategories || sidebarEnabled) {
4636                let productsContent = AppendChildren(response, 1);
4637                if (sidebarEnabled) {
4638                    $(".user-navbar").after(
4639                        $("<div>")
4640                            .addClass("side-navbar")
4641                            .append($("<div>").addClass("container").append("<ul>").attr("role", "menu").html(productsContent)),
4642                    );
4643                    $("body").addClass("sidebarEnabled");
4644                }
4645                if (manyCategories) {
4646                    html_ = `<li class="nav-item dropdown has-megamenu category_zero" role="none"><a class="nav-link dropdown-toggle" role="menuitem" href="#" ${
4647                        hoverNavItems && !mobile ? ">" : 'data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'
4648                    }${$(".navbar-nav").attr("productHeader")}</a>
4649                    <div class="dropdown-menu singular-menu megamenu" role="none"><div class="container" role="none">${productsContent}</div></div></li><li class="nav-item TemplateMenu_AboutUs" role="none"><a class="nav-link" role="menuitem" href="/about-us">About us</a></li>`;
4650
4651                    $(".ulLoop_1 a").each(function () {
4652                        if ($(this).next("ul").length && $(this).next("ul").css("display") == "none") {
4653                            $(this).next("ul").addClass("visibleToggle");
4654                            $(this).next("ul").parent().addClass("visibleToggleSub");
4655                        }
4656                    });
4657                    $('.ulLoop_1 a[href="' + location.pathname + '"]').addClass("active");
4658                    $(".ulLoop_1 .active").parents("ul").show();
4659                }
4660            } else {
4661                html_ = "";
4662                for (i in response) {
4663                    let catId = response[i].syncID;
4664                    let activeCategory = catId == currentCategory ? " activeCat" : "";
4665                    if (response[i].children.length) {
4666                        html_ += `<li class="nav-item dropdown has-megamenu category_${catId}" role="none">
4667                    <a class="nav-link dropdown-toggle${activeCategory}" role="menuitem" data-category="${catId}" href="${
4668                        response[i].url
4669                    }" ${hoverNavItems && !mobile ? ">" : ' data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'}${
4670                        response[i].name
4671                    }</a>
4672                    <div class="dropdown-menu megamenu" role="none"><div class="container" role="none">${AppendChildren(
4673                        response[i].children,
4674                        1,
4675                    )}</div></div></li>`;
4676                    } else {
4677                        html_ += `<li class="nav-item category_${catId}" role="none"><a class="nav-link${activeCategory}" data-category="${catId}" href="${response[i].url}" role="menuitem">${response[i].name}</a></li>`;
4678                    }
4679                }
4680            }
4681
4682            $("#scMenu").replaceWith(html_);
4683            screenWidth = $(window).width();
4684            checkWidthDependantElements();
4685            //callback
4686            onNavbarCreated();
4687        });
4688}
4689function AppendChildren(menu, level) {
4690    let html_ = "";
4691    for (i in menu) {
4692        activeCategory = menu[i].syncID == currentCategory ? " activeCat" : "";
4693        html_ += `<li class="level${level} category_${menu[i].syncID}" role="none"><a href="${
4694            menu[i].url
4695        }" class="dropdown-link${activeCategory}" data-category="${
4696            menu[i].syncID
4697        }" role="menuitem" aria-haspopup=\"true\" aria-expanded=\"true\"">${menu[i].name}</a>${AppendChildren(
4698            menu[i].children,
4699            level + 1,
4700        )}</li>`;
4701    }
4702
4703    switch (level) {
4704        case 1:
4705            return html_.length > 0
4706                ? '<ul role="menu" class="d-lg-flex ulLoop_' + level + '">' + html_ + "</ul>"
4707                : "";
4708        default:
4709            return html_.length > 0 ? '<ul role="menu" class="ulLoop_' + level + '">' + html_ + "</ul>" : "";
4710    }
4711}
4712function adjustSideControlsHeight() {
4713    let scrollTop = $(window).scrollTop();
4714    let windowHeight = $(window).height();
4715    let bodyHeight = $("body").outerHeight();
4716    let footerProtrusion = windowHeight + scrollTop - bodyHeight;
4717    $(".galleryControls").css("padding-bottom", footerProtrusion + "px");
4718    $(".sidebarEnabled .side-navbar").css("padding-bottom`", footerProtrusion + "px");
4719}
4720
4721// --- UTILITIES --- //
4722
4723function changeAccount(elm) {
4724    const guid = $(elm).attr("value");
4725    if (guid != "") {
4726        document.location.href = "/accountselect/" + guid;
4727    }
4728}
4729
4730function addCommas(nStr) {
4731    if (bAddComma) {
4732        nStr += "";
4733        x = nStr.split(".");
4734        x1 = x[0];
4735        x2 = x.length > 1 ? "," + x[1] : "";
4736        let rgx = /(\d+)(\d{3})/;
4737        while (rgx.test(x1)) {
4738            x1 = x1.replace(rgx, "$1" + "." + "$2");
4739        }
4740        return x1 + x2;
4741    } else {
4742        nStr += "";
4743        x = nStr.split(".");
4744        x1 = x[0];
4745        x2 = x.length > 1 ? "." + x[1] : "";
4746        let rgx = /(\d+)(\d{3})/;
4747        while (rgx.test(x1)) {
4748            x1 = x1.replace(rgx, "$1" + "," + "$2");
4749        }
4750        return x1 + x2;
4751    }
4752}
4753function isEmail(email) {
4754    let regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/;
4755    return regex.test(email);
4756}
4757function sendEmail() {
4758    $("#email").css("border", "var(--inputBorder)");
4759    $(".emailSent").hide();
4760
4761    let email = $("#email").val();
4762    let data_ = JSON.stringify({ email: email });
4763
4764    if (email == "") {
4765        $("#email").css("border", "red solid 1px");
4766    }
4767    if (!email.includes("@") || !email.split("@")[1].includes(".")) {
4768        //QQX Needs label
4769        alert("Please enter a valid email address.");
4770    } else {
4771        $.ajax({
4772            url: "/api/login/resetPassword/",
4773            method: "POST",
4774            contentType: "application/json",
4775            data: data_,
4776            cache: false,
4777        })
4778            .fail(function () {
4779                //QQX Needs label
4780                alert("Failed to login");
4781            })
4782            .done(function (response) {
4783                if (response.success) {
4784                    $(".emailSent").show();
4785                } else {
4786                    alert(response.error_msg);
4787                }
4788            });
4789    }
4790}
4791function AddToCartBtn() {
4792    $("*[onAdd]").each(function () {
4793        $(this).addClass("addToCartAni");
4794    });
4795}
4796function validateForm() {
4797    $(".loginError").addClass("d-none");
4798
4799    let username = $("#username").val();
4800    let password = $("#password").val();
4801    let security_code = "";
4802    let data_ = JSON.stringify({ username: username, password: password });
4803
4804    if (username == "" || password == "") {
4805        $(".loginError").removeClass("d-none");
4806    } else {
4807        $("body").addClass("loading");
4808        let button = $("button[type=submit]");
4809        let spanContent = $(button).find("span").html();
4810
4811        $(button).css({
4812            width: $(button).outerWidth() + "px",
4813            height: $(button).outerHeight() + "px",
4814        });
4815        $(button).find("span").html("");
4816        $(button).addClass("loading");
4817        $.ajax({
4818            url: "/api/login",
4819            method: "POST",
4820            contentType: "application/json",
4821            data: data_,
4822            cache: false,
4823        })
4824            .fail(function () {
4825                //QQX Needs label
4826                alert("Failed to login");
4827                $("body").removeClass("loading");
4828            })
4829            .done(function (response) {
4830                $("body").removeClass("loading");
4831                if (response.success) {
4832                    if (response.salesperson) {
4833                        location.href = "/salesperson";
4834                        return true;
4835                    }
4836
4837                    if (response.redirect_url != null && response.redirect_url != "") {
4838                        location.href = response.redirect_url;
4839                        return true;
4840                    }
4841
4842                    if (response.change_password) {
4843                        location.href =
4844                            "/changepassword?user=" + encodeURIComponent(username) + "&security_code=" + encodeURIComponent(security_code);
4845                        return true;
4846                    }
4847                    if (response.is_salesperson) {
4848                    }
4849                    // redirect to homepage
4850                    else {
4851                        $("#username").val("");
4852                        $("#password").val("");
4853
4854                        if (location.href.indexOf("login") != -1 || location.href.indexOf("logout") != -1) location.href = "/";
4855                        else location.reload();
4856                    }
4857                } else {
4858                    $(button).find("span").html(spanContent);
4859                    $(button).css({ width: "", height: "" });
4860                    $(button).removeClass("loading");
4861                    $("#password").val("");
4862                    $(".loginError").removeClass("d-none");
4863                    if (response.error_msg != "") {
4864                        $(".loginError").html(response.error_msg);
4865                    }
4866                    $(".resetPassword").attr("href", "/resetpassword/?user=" + encodeURIComponent(username));
4867                }
4868            });
4869    }
4870    return false;
4871}
4872function checkMinMaxDecimals(input) {
4873    //input variables
4874    let value = $(input).val();
4875    const step = parseFloat($(input).attr("step"));
4876    const min = parseFloat($(input).attr("min"));
4877    const max = parseFloat($(input).attr("max"));
4878    const type = $(input).attr("type");
4879    let containsComma = false;
4880
4881    //correct comma
4882    if (type == "text") {
4883        containsComma = value.includes(",");
4884        value = parseFloat(value.replaceAll(",", "."));
4885    }
4886
4887    //correct decimals
4888    let decimalPointMultiplier = Math.pow(10, $(input).data("decimals"));
4889    if (!decimalPointMultiplier) decimalPointMultiplier = 1;
4890    value = Math.round(value * decimalPointMultiplier) / decimalPointMultiplier;
4891
4892    //correct min and step
4893    if (!isNaN(min) && value < min) value = min;
4894    if (!isNaN(max) && value > max) value = max;
4895    if (!isNaN(step) && value) value = Math.floor(value / step) * step;
4896
4897    //re-correct comma
4898    if (type == "text") {
4899        if (containsComma) value = value.toString().replace(".", ",");
4900        else value = value.toString();
4901    }
4902
4903    //set value
4904    $(input).val(value).change();
4905}
4906// Cookies
4907function setCookie(cname, cvalue, exdays) {
4908    const d = new Date();
4909    d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
4910    let expires = "expires=" + d.toUTCString();
4911    document.cookie = cname + "=" + encodeURIComponent(cvalue) + ";" + expires + ";path=/";
4912}
4913function deleteCookie(cname) {
4914    const d = new Date();
4915    d.setTime(d.getTime() + 24 * 60 * 60 * 1000);
4916    let expires = "expires=" + d.toUTCString();
4917    document.cookie = cname + "=;" + expires + ";path=/";
4918}
4919function getCookie(cname) {
4920    let name = cname + "=";
4921    let decodedCookie = decodeURIComponent(document.cookie);
4922    let ca = decodedCookie.split(";");
4923    for (let i = 0; i < ca.length; i++) {
4924        let c = ca[i];
4925        while (c.charAt(0) == " ") {
4926            c = c.substring(1);
4927        }
4928        if (c.indexOf(name) == 0) {
4929            return c.substring(name.length, c.length);
4930        }
4931    }
4932    return "";
4933}
4934function acceptCookieConsent() {
4935    deleteCookie("user_cookie_consent");
4936    setCookie("user_cookie_consent", 1, 30);
4937    document.getElementById("cookieNotice").style.display = "none";
4938}
4939function AcceptTerms(elm) {
4940    if ($(".AcceptTerms").is(":checked")) {
4941        $("#completeOrderCreditCard").removeAttr("disabled");
4942    } else {
4943        $("#completeOrderCreditCard").attr("disabled", "disabled");
4944    }
4945}
4946function setOrDeleteCookie(id, elm) {
4947    if ((elm.type == "checkbox" && elm.checked) || (elm.type != "checkbox" && elm.value.trim().length)) {
4948        setCookie(id + "-" + elm.name, elm.value);
4949    } else {
4950        deleteCookie(id + "-" + elm.name);
4951    }
4952}
4953// Helpers
4954function isPasswordComplexEnough(password) {
4955    if (!password || password.length < 6) {
4956        return false;
4957    }
4958    let hasUpper = /[A-Z]/.test(password);
4959    let hasNumberOrSpecial = /[^A-Za-z]/.test(password);
4960    return hasUpper && hasNumberOrSpecial;
4961}
4962function resetPassword() {
4963    $("#new_password").css("border", "1px solid black");
4964    $("#confirm_password").css("border", "1px solid black");
4965    $(".passNotMatching").hide();
4966    $(".passwordUpdated").hide();
4967    $(".passNotUpdated").hide();
4968    $(".passwordNotComplex").hide();
4969    $(".resetLinkInvalid").hide();
4970
4971    let newPass = $("#new_password").val();
4972    let confirmPass = $("#confirm_password").val();
4973    let cs = $("#cs").val();
4974    let data_ = JSON.stringify({ newPass: newPass, cs: cs });
4975
4976    if (newPass == "") {
4977        $("#new_password").css("border", "red solid 1px");
4978    }
4979    if (confirmPass == "") {
4980        $("#confirm_password").css("border", "red solid 1px");
4981    }
4982    let elm = $("button[type=submit]");
4983    // better check: use a regular expression
4984    if (newPass != confirmPass) {
4985        $(".passNotMatching").show();
4986        buttonAlert(elm, "error");
4987    } else if (newPass != "") {
4988        if ($("#enforceComplexPassword").val() == "1" && !isPasswordComplexEnough(newPass)) {
4989            $(".passwordNotComplex").show();
4990            $("#new_password").css("border", "red solid 1px");
4991            buttonAlert(elm, "error");
4992            return;
4993        }
4994        let spanContent = $(elm).find("span").html();
4995        $(elm).css({
4996            width: $(elm).outerWidth() + "px",
4997            height: $(elm).outerHeight() + "px",
4998        });
4999        $(elm).find("span").html("");
5000        $(elm).addClass("loading");
5001        let classToAdd = "success";
5002        $.ajax({
5003            url: "/api/login/resetPasswordCompleted",
5004            method: "POST",
5005            contentType: "application/json",
5006            data: data_,
5007            cache: false,
5008        })
5009            .fail(function () {
5010                alert("Failed to update password");
5011                classToAdd = "error";
5012            })
5013            .done(function (response) {
5014                if (response.success) {
5015                    $("#new_password").val();
5016                    $("#confirm_password").val();
5017                    $(".passwordUpdated").show();
5018                    //Redirect to login after 3 seconds.
5019                    setTimeout(function () {            
5020                      window.location.href='/login';
5021                    }, 3000);
5022                } else {
5023                    classToAdd = "error";
5024                    if (response.status) {
5025                        $(".resetLinkInvalid").show();
5026                    } else {
5027                        $(".passNotUpdated").show();
5028                    }
5029                }
5030            })
5031            .always(function () {
5032                buttonAlert($(elm), classToAdd, spanContent);
5033            });
5034    }
5035}
5036function buttonAlert(elm, classToAdd, spanContent = null) {
5037    $(elm).removeClass("loading").addClass(classToAdd);
5038    $(elm).attr("last-class", classToAdd);
5039    setTimeout(function () {
5040        $(elm).removeClass(classToAdd);
5041        if (spanContent) $(elm).find("span").html(spanContent);
5042        $(elm).css({ width: "", height: "" });
5043        setTimeout(function () {
5044            $(elm).blur();
5045        }, 350);
5046    }, 3000);
5047}
5048function selectFinder(elm, specCode, level) {
5049    let val = $(elm).val();
5050
5051    if (val == "_") {
5052        deleteFinderLevel(level);
5053        return;
5054    }
5055    setCookie("_scFinderFilter" + level, specCode + "!" + val);
5056    deleteCookie("_scFinderFilter" + (level + 1));
5057    location.reload();
5058}
5059function deleteFinderLevel(level) {
5060    $(".productFinderItem").eq(level).find("select").val("_");
5061    for (let c = level; c < 20; c++) {
5062        deleteCookie("_scFinderFilter" + c);
5063    }
5064    location.reload();
5065}
5066function validateChangePassForm() {
5067    $(".loginError").addClass("d-none");
5068    $("#old_password").css("border", "1px solid black");
5069    $("#new_password").css("border", "1px solid black");
5070    $("#confirm_password").css("border", "1px solid black");
5071
5072    let old_password = $("#old_password").val();
5073    let new_password = $("#new_password").val();
5074    let confirm_password = $("#confirm_password").val();
5075    const queryString = window.location.search;
5076
5077    let user = new URLSearchParams(queryString).get("user");
5078    let data_ = JSON.stringify({
5079        username: user,
5080        old_password: old_password,
5081        new_password: new_password,
5082        confirm_password: confirm_password,
5083    });
5084    let authenticated = false;
5085    let button = $("button[type=submit]");
5086    let spanContent = $(button).find("span").html();
5087    if (old_password == "" || new_password == "" || confirm_password == "") {
5088        if (old_password == "") {
5089            $("#old_password").css("border", "red solid 1px");
5090        }
5091        if (new_password == "") {
5092            $("#new_password").css("border", "red solid 1px");
5093        }
5094        if (confirm_password == "") {
5095            $("#confirm_password").css("border", "red solid 1px");
5096        }
5097    } else if (new_password == old_password) {
5098        $("#new_password").css("border", "red solid 1px");
5099        $(".loginErrorMessage").html($(".passwordSameAsOldMsg").html());
5100        $(".loginError").removeClass("d-none");
5101    } else if ($("#enforceComplexPassword").val() == "1" && !isPasswordComplexEnough(new_password)) {
5102        $("#new_password").css("border", "red solid 1px");
5103        $(".loginErrorMessage").html($(".passwordNotComplexMsg").html());
5104        $(".loginError").removeClass("d-none");
5105    } else {
5106        $(button).css({
5107            width: $(button).outerWidth() + "px",
5108            height: $(button).outerHeight() + "px",
5109        });
5110        $(button).find("span").html("");
5111        $(button).addClass("loading");
5112        $.ajax({
5113            url: "/api/login/changepassword",
5114            method: "POST",
5115            contentType: "application/json",
5116            data: data_,
5117            cache: false,
5118        })
5119            .fail(function () {
5120                alert("failed");
5121            })
5122            .done(function (response) {
5123                if (response.success) {
5124                    location.href = "/";
5125                } else {
5126                    $(button).find("span").html(spanContent);
5127                    $(button).css({ width: "", height: "" });
5128                    $(button).removeClass("loading");
5129
5130                    $("#old_password").val("");
5131                    $("#new_password").val("");
5132                    $("#confirm_password").val("");
5133
5134                    $(".loginError").removeClass("d-none");
5135                    if (response.error_msg != "") {
5136                        $(".loginErrorMessage").html(response.error_msg);
5137                    }
5138                }
5139            });
5140    }
5141    return false;
5142}
5143function requiredCreateCustomerFieldsFilled() {
5144    let allFilled = true;
5145    let focusSet = false;
5146
5147    $(".createAccount")
5148        .find("form")
5149        .find("input")
5150        .each(function () {
5151            $(this).css("border", "");
5152        });
5153
5154    $(".createAccount")
5155        .find("form")
5156        .find("input")
5157        .each(function () {
5158            if ($(this).prop("required")) {
5159                if ($(this).val() == undefined || $(this).val() == null || $(this).val() == "") {
5160                    $(this).css("border", "1px red solid");
5161                    if (!focusSet) {
5162                        focusSet = true;
5163                        $(this).trigger("focus");
5164                    }
5165                    allFilled = false;
5166                    $("#error_fields").show();
5167                }
5168            }
5169        });
5170
5171    if (!isEmail($("#email").val())) {
5172        $("#email").css("border", "1px red solid");
5173        allFilled = false;
5174    }
5175    return allFilled;
5176}
5177function changeCustomerType() {
5178    if ($(".scShopCheckout").length) {
5179        switch ($("#CustomerType").val()) {
5180            case "PRIVATE":
5181                if ($("#" + $("#CustomerType").val()).not(":visible")) {
5182                    $("#" + $("#CustomerType").val()).show();
5183                    $("#COMPANY").hide();
5184                    $("#PRIVATE input, #PRIVATE textarea, #PRIVATE select").removeAttr("disabeld");
5185                    $("#COMPANY input, #COMPANY textarea, #COMPANY select").attr("disabeld", "disabeld");
5186                }
5187                break;
5188            case "COMPANY":
5189                if ($("#" + $("#CustomerType").val()).not(":visible")) {
5190                    $("#" + $("#CustomerType").val()).show();
5191                    $("#PRIVATE").hide();
5192                    $("#COMPANY input, #COMPANY textarea, #COMPANY select").removeAttr("disabeld");
5193                    $("#PRIVATE input, #PRIVATE textarea, #PRIVATE select").attr("disabeld", "disabeld");
5194                }
5195                break;
5196        }
5197    }
5198}
5199function debounce(func, delay) {
5200    let timeout;
5201    return function (...args) {
5202        const context = this;
5203        clearTimeout(timeout);
5204        timeout = setTimeout(() => func.apply(context, args), delay);
5205    };
5206}
5207function checkAmountValidity(elm) {
5208    const value = parseInt($(elm).val());
5209    if ($(elm).attr("allow-zero") === "allow" && value == 0) {
5210        return;
5211    }
5212    if (value === "" || value < $(elm).attr("min") || value < 0 || isNaN(value)) {
5213        $(elm).val($(elm).attr("min"));
5214        return;
5215    }
5216    if (value > $(elm).attr("max")) {
5217        $(elm).val($(elm).attr("max"));
5218        return;
5219    }
5220    let step = parseFloat($(elm).attr("step"));
5221    if (step) {
5222        let roundedValue = Math.round(value / step) * step;
5223        while (roundedValue < $(elm).attr("min")) {
5224            roundedValue += step;
5225        }
5226        $(elm).val(roundedValue.toString());
5227    }
5228}
5229// Favorites
5230function toggleFavoritesDropdown(button) {
5231    if (
5232        $(".ProductMatrixAmountInput, .ProductAmountInput").length &&
5233        !$(".ProductMatrixAmountInput, .ProductAmountInput").filter(function () {
5234            return $(this).val() != 0;
5235        }).length
5236    ) {
5237        $.toast({
5238            heading: "Missing quantity",
5239            text: "Please set an amount for at least one variant",
5240            icon: "error",
5241            stack: false,
5242            hideAfter: 5000,
5243            position: "bottom-right",
5244        });
5245        return;
5246    }
5247    if ($(".pageViewBasket").length && !$(".basketRowLines > .basketRowLine").length) {
5248        $.toast({
5249            heading: "Missing quantity",
5250            text: "No items in the basket",
5251            icon: "error",
5252            stack: false,
5253            hideAfter: 5000,
5254            position: "bottom-right",
5255        });
5256        return;
5257    }
5258    $(button).toggleClass("showDropdown");
5259    if ($(button).hasClass("showDropdown")) {
5260        // Load favorite lists
5261        let dropdown = $(button).siblings(".favoritesDropdown");
5262
5263        // Reset the dropdown to its initial state
5264        dropdown.empty().removeClass("listFetched");
5265        $(button).addClass("loading");
5266        $.get("/favorites/getlists", function (data) {
5267            let content = $("<ul>");
5268
5269            // Display existing favorite lists
5270            if (data.length > 0) {
5271                data.forEach(function (list) {
5272                    let desc = $("<span>").html(list.description);
5273                    let btn = $("<button>")
5274                        .addClass("btn btn-icon btn-success submitBtn")
5275                        .append($("<span>").append($("<i>").addClass("fas fa-plus")));
5276                    let starBtn = $(dropdown).siblings(".favoriteStarButton");
5277                    $(btn).on("click", function () {
5278                        addToFavorite(list.code, starBtn, $(btn).siblings("span").html());
5279                    });
5280                    let option = $("<li>").append(desc, btn);
5281                    content.append(option);
5282                });
5283            }
5284            let createListBtn = $("<button>").addClass("btn btn-primary").append($("<span>").html("Create New List"));
5285            $(createListBtn).on("click", function () {
5286                $(this).addClass("editName");
5287                $(newListInput).trigger("focus");
5288            });
5289            let newFavListForm = $("<form>").addClass("newFavListForm");
5290            let newListInput = $("<input>")
5291                .attr("type", "text")
5292                .attr("placeholder", "Enter new list name")
5293                .addClass("newListInput form-control");
5294            let submitBtn = $("<button>").addClass("btn btn-primary").attr("type", "submit").append($("<span>").html("Create List"));
5295            $(newFavListForm).on("submit", function (e) {
5296                e.preventDefault();
5297                let listName = $(newListInput).val();
5298                if (listName.trim() !== "") {
5299                    $.post("/favorites/add", { listName: listName }, function () {
5300                        $.get("/favorites/getlists", function (newData) {
5301                            let newList = newData.find((list) => list.description === listName);
5302                            if (newList) {
5303                                let starBtn = $(dropdown).siblings(".favoriteStarButton");
5304                                addToFavorite(newList.code, starBtn, listName); // Add item to the new list
5305                                $(dropdown).removeClass("listFetched"); // Force reload on next dropdown open
5306                                toggleFavoritesDropdown(starBtn); // Close and reopen to refresh the list
5307                            } else {
5308                                console.error("Failed to find new list in response data");
5309                            }
5310                        }).fail(function () {
5311                            console.error("Failed to fetch new list data");
5312                        });
5313                    }).fail(function () {
5314                        console.error("Failed to create new list");
5315                    });
5316                } else {
5317                    $(this).find(".newListInput").addClass("form-error").trigger("focus");
5318                }
5319            });
5320
5321            $(newListInput).on("input", function () {
5322                $(this).removeClass("form-error");
5323            });
5324
5325            newFavListForm.append(newListInput, submitBtn);
5326            let createListSection = $("<li>").addClass("createListSection").append(createListBtn, newFavListForm);
5327            $(content).append(createListSection);
5328            dropdown.append(content).addClass("listFetched");
5329            $(button).removeClass("loading");
5330        }).fail(function () {
5331            $(btn).removeClass("success error");
5332        });
5333        return;
5334    }
5335}
5336function addToFavorite(favoriteCode, btn, favListName) {
5337    $(btn).addClass("loading").removeClass("showDropdown");
5338
5339    if ($(".pageViewBasket").length) {
5340        postBasketToFavoriteList(favoriteCode, btn, favListName);
5341        return;
5342    }
5343    if ($(".ProductMatrixTable, .ProductBCVariantDropDown, .variantTypeList, .variantTypeMatrix").length) {
5344        addMultipleProductsToFavorites(favoriteCode, btn, favListName);
5345        return;
5346    }
5347    postProductToFavoriteList(favoriteCode, btn, favListName);
5348    return;
5349}
5350function postProductToFavoriteList(favoriteCode, btn, favListName) {
5351    let thisBuy = $(btn).closest(".buyBox");
5352    let itemNo = $(thisBuy).data("sku");
5353    let variantCode = $(thisBuy).data("variant");
5354    let uomCode = $(thisBuy).data("uom");
5355    let qtyInput = $(thisBuy).find(".ProductAmountInput");
5356    let qty = 1;
5357    if (qtyInput.length) {
5358        qty = $(qtyInput).val();
5359    }
5360    $.post("/favorites/lines/add", {
5361        favoriteCode: favoriteCode,
5362        itemNo: itemNo,
5363        variantCode: variantCode,
5364        unitOfMeasureCode: uomCode,
5365        quantity: qty,
5366    })
5367        .done(function () {
5368            $(btn).addClass("success").attr("last-class", "success");
5369            showAddedToFavsToast(favoriteCode, favListName);
5370        })
5371        .fail(function (xhr) {
5372            $(btn).addClass("error").attr("last-class", "error");
5373        })
5374        .always(function (textStatus) {
5375            $(btn).removeClass("loading");
5376            setTimeout(function () {
5377                $(btn).removeClass("success error");
5378            }, 3000);
5379        });
5380}
5381function postBasketToFavoriteList(favoriteCode, btn, favListName) {
5382    $.ajax({
5383        url: "favorites/savebasket/?favoriteCode=" + encodeURIComponent(favoriteCode),
5384        method: "POST",
5385        cache: false,
5386        contentType: "application/json",
5387    })
5388        .done(function () {
5389            $(btn).addClass("success").attr("last-class", "success");
5390            showAddedToFavsToast(favoriteCode, favListName);
5391        })
5392        .fail(function (xhr) {
5393            $(btn).addClass("error").attr("last-class", "error");
5394        })
5395        .always(function () {
5396            $(btn).removeClass("loading");
5397            setTimeout(function () {
5398                $(btn).removeClass("success error");
5399            }, 3000);
5400        });
5401}
5402function addMultipleProductsToFavorites(favoriteCode, btn, favListName) {
5403    let selectedVariants = [];
5404
5405    //product matrix
5406    $(".ProductMatrixAmountInput").each(function (index) {
5407        if (this.value == 0) return;
5408        const variantId = $("input.ProductMatrixVariantId")[index];
5409        const itemNo = $("input.ProductMatrixItemNo")[index];
5410        const itemUOM = $("input.ProductMatrixUOM")[index];
5411
5412        const variantData = {
5413            ItemNo: itemNo.value,
5414            VariantCode: VariantId ? variantId.value : null,
5415            UnitOfMeasureCode: itemUOM.value,
5416            Quantity: parseInt(this.value),
5417        };
5418
5419        selectedVariants.push(variantData);
5420    });
5421
5422    //NewPriceAndVariantRender matrix and list
5423    $(".variantContainer .variantItem").each(function () {
5424        let itemQty = parseInt($(this).find(".incDecInput").val());
5425        if (itemQty == 0) return;
5426        let itemNo = String($(this).data("itemno"));
5427        let variantId = String($(this).data("variant"));
5428        let itemUOM = $(this).data("uom");
5429        let variantData = {
5430            ItemNo: itemNo,
5431            VariantCode: variantId ? variantId : null,
5432            UnitOfMeasureCode: itemUOM,
5433            Quantity: itemQty,
5434        };
5435
5436        selectedVariants.push(variantData);
5437        $(this).addClass("valueChanged");
5438    });
5439
5440    // bc variants list
5441    $(".favItem").each(function () {
5442        let itemQty = parseInt($(this).find(".incDecInput").val());
5443        if (itemQty == 0) return;
5444        let itemNo = String($(this).data("itemno"));
5445        let variantId = String($(this).data("variant"));
5446        let itemUOM = $(this).data("uom");
5447        let variantData = {
5448            ItemNo: itemNo,
5449            VariantCode: variantId ? variantId : null,
5450            UnitOfMeasureCode: itemUOM,
5451            Quantity: itemQty,
5452        };
5453
5454        selectedVariants.push(variantData);
5455        $(this).addClass("valueChanged");
5456    });
5457
5458    if (selectedVariants.length > 0) {
5459        $(btn).addClass("loading");
5460
5461        $.ajax({
5462            url: "/favorites/lines/addmultiple",
5463            method: "POST",
5464            contentType: "application/json",
5465            data: JSON.stringify({
5466                favoriteCode: favoriteCode,
5467                lines: selectedVariants,
5468            }),
5469        })
5470            .done(function () {
5471                $(btn).addClass("success").attr("last-class", "success");
5472                showAddedToFavsToast(favoriteCode, favListName);
5473            })
5474            .fail(function () {
5475                $(btn).addClass("error").attr("last-class", "error");
5476            })
5477            .always(function () {
5478                $(btn).removeClass("loading");
5479                setTimeout(function () {
5480                    $(btn).removeClass("success error");
5481                }, 3000);
5482            });
5483    } else {
5484        console.error("No variants selected to add to favorites.");
5485        $(btn).removeClass("loading");
5486        $(btn).addClass("error").attr("last-class", "error");
5487        setTimeout(function () {
5488            $(btn).removeClass("success error");
5489        }, 1500);
5490    }
5491}
5492function removeProduct(lineId = null, btn = null, cancel = false) {
5493    let thisDelete = $(btn).parent();
5494    let thisRow = $(thisDelete).closest("tr");
5495    if (cancel) {
5496        $(thisDelete).removeClass("show");
5497        return;
5498    }
5499    if (!$(thisDelete).hasClass("show")) {
5500        $(thisDelete).addClass("show");
5501        return;
5502    }
5503    $(btn).addClass("loading");
5504    $("body").addClass("loading");
5505    $(thisRow).css("max-height", $(thisRow).outerHeight() + "px");
5506    $.post("/favorites/lines/remove", {
5507        lineId: lineId,
5508    })
5509        .done(function (data) {
5510            $(btn).addClass("success").attr("last-class", "success");
5511            $(thisRow).addClass("deleting");
5512            setTimeout(function () {
5513                $(thisRow).remove();
5514            }, 500);
5515        })
5516        .fail(function (xhr) {
5517            $(btn).addClass("error").attr("last-class", "error");
5518        })
5519        .always(function () {
5520            $("body").removeClass("loading");
5521        })
5522        .always(function () {
5523            setTimeout(function () {
5524                $(btn).removeClass("success error");
5525            }, 3000);
5526        });
5527}
5528function showAddedToFavsToast(favoriteCode, name) {
5529    $.toast({
5530        text: `Added to favorite list:<br><a href="/favorites/lines/${favoriteCode}">${name}</a>`,
5531        icon: "success",
5532        stack: false,
5533        hideAfter: 5000,
5534        position: "bottom-right",
5535    });
5536}
5537// ASYNC Functions
5538async function cartCommentFileChanged(elm) {
5539    let file = $(elm).prop("files")[0];
5540
5541    if (file == null || elm == "") {
5542        return;
5543    }
5544
5545    let filedata = await getBase64(file);
5546
5547    $(".modal-commentline .cloneRow")
5548        .not(".d-none")
5549        .find("input.basketLineComments")
5550        .each(function () {
5551            if ($(this).data("masterdatavalue") === $(elm).data("masterdatavalue")) {
5552                $(this).attr("data-hasfile", "true");
5553                $(this).attr("data-filedata", filedata);
5554                $(this).siblings("span").html(file.name);
5555            }
5556        });
5557}
5558async function getBase64(file) {
5559    return new Promise((resolve, reject) => {
5560        let reader = new FileReader();
5561        reader.onload = () => resolve(reader.result);
5562        reader.onerror = (e) => reject(e);
5563        reader.readAsDataURL(file);
5564    });
5565}
5566// ---- CUSTOM ELEMENTS ---- //
5567customElements.define(
5568    "double-slider",
5569    class DoubleSlider extends HTMLElement {
5570        static get observedAttributes() {
5571            return ["min", "max", "step", "value-min", "value-max"];
5572        }
5573
5574        constructor() {
5575            super();
5576            this._built = false;
5577            this._reflecting = false;
5578            this._activeNode = null;
5579            this._dragStart = null;
5580            this._dragFraction = 0;
5581            this._rect = null;
5582            this._size = null;
5583            this._commitTimer = null;
5584        }
5585
5586        connectedCallback() {
5587            if (!this._built) {
5588                this._build();
5589            }
5590            this._sync();
5591        }
5592
5593        attributeChangedCallback() {
5594            if (this._built && !this._reflecting) {
5595                this._sync();
5596            }
5597        }
5598
5599        get min() {
5600            return this._parse(this.getAttribute("min"), 0);
5601        }
5602
5603        get max() {
5604            const max = this._parse(this.getAttribute("max"), 100);
5605            return max > this.min ? max : this.min;
5606        }
5607
5608        //Zero means continuous, matching step="any" on a native range input.
5609        get step() {
5610            const raw = this.getAttribute("step");
5611            if (raw === null || raw === "any") {
5612                return 0;
5613            }
5614            const step = parseFloat(raw);
5615            return isNaN(step) || step <= 0 ? 0 : step;
5616        }
5617
5618        get nudge() {
5619            return this.step || (this.max - this.min) / 100;
5620        }
5621
5622        get valueMin() {
5623            return this._limit(this._parse(this.getAttribute("value-min"), this.min));
5624        }
5625
5626        set valueMin(value) {
5627            this._write(value, this.valueMax);
5628        }
5629
5630        get valueMax() {
5631            return this._limit(this._parse(this.getAttribute("value-max"), this.max));
5632        }
5633
5634        set valueMax(value) {
5635            this._write(this.valueMin, value);
5636        }
5637
5638        get isDefault() {
5639            return this.valueMin <= this.min && this.valueMax >= this.max;
5640        }
5641
5642        //Silent, so a caller clearing several sliders can submit once for all of them.
5643        reset() {
5644            clearTimeout(this._commitTimer);
5645            this._write(this.min, this.max);
5646        }
5647
5648        _build() {
5649            this.innerHTML = `
5650                <input type="number" class="sliderValue min" inputmode="decimal">
5651                <div class="sliderTrack">
5652                    <div class="sliderRange"></div>
5653                    <div class="sliderNode min" role="slider" tabindex="0" aria-orientation="horizontal"></div>
5654                    <div class="sliderNode max" role="slider" tabindex="0" aria-orientation="horizontal"></div>
5655                </div>
5656                <input type="number" class="sliderValue max" inputmode="decimal">`;
5657            this._track = this.querySelector(".sliderTrack");
5658            this._nodes = {
5659                min: this.querySelector(".sliderNode.min"),
5660                max: this.querySelector(".sliderNode.max"),
5661            };
5662            this._inputs = {
5663                min: this.querySelector(".sliderValue.min"),
5664                max: this.querySelector(".sliderValue.max"),
5665            };
5666            ["min", "max"].forEach((which) => {
5667                const label = this.getAttribute("label-" + which) || (which === "min" ? "Minimum" : "Maximum");
5668                this._nodes[which].setAttribute("aria-label", label);
5669                this._inputs[which].setAttribute("aria-label", label);
5670                this._inputs[which].addEventListener("input", () => this._onFieldInput(which));
5671                this._inputs[which].addEventListener("blur", () => this._commitField(which, true));
5672                this._inputs[which].addEventListener("keydown", (e) => this._onFieldKeyDown(e, which));
5673                //The fields are internal, so their native change never reaches the page.
5674                this._inputs[which].addEventListener("change", (e) => e.stopPropagation());
5675            });
5676            this._track.addEventListener("pointerdown", (e) => this._onPointerDown(e));
5677            this._track.addEventListener("pointermove", (e) => this._onPointerMove(e));
5678            this._track.addEventListener("pointerup", (e) => this._onPointerUp(e));
5679            this._track.addEventListener("pointercancel", () => this._onPointerCancel());
5680            this.addEventListener("keydown", (e) => this._onKeyDown(e));
5681            this._built = true;
5682        }
5683
5684        _parse(raw, fallback) {
5685            const value = parseFloat(raw);
5686            return isNaN(value) ? fallback : value;
5687        }
5688
5689        _limit(value) {
5690            if (value < this.min) {
5691                return this.min;
5692            }
5693            if (value > this.max) {
5694                return this.max;
5695            }
5696            return value;
5697        }
5698
5699        _snap(value) {
5700            const step = this.step;
5701            if (!step) {
5702                return this._limit(value);
5703            }
5704            const steps = Math.round((value - this.min) / step);
5705            return this._limit(parseFloat((this.min + steps * step).toFixed(10)));
5706        }
5707
5708        //Single point of truth: normalises, reflects to attributes and repaints.
5709        _write(low, high) {
5710            let newLow = this._snap(low);
5711            let newHigh = this._snap(high);
5712            if (newLow > newHigh) {
5713                const swap = newLow;
5714                newLow = newHigh;
5715                newHigh = swap;
5716            }
5717            this._reflecting = true;
5718            this.setAttribute("value-min", String(newLow));
5719            this.setAttribute("value-max", String(newHigh));
5720            if (newLow <= this.min && newHigh >= this.max) {
5721                this.removeAttribute("modified");
5722            } else {
5723                this.setAttribute("modified", "");
5724            }
5725            this._reflecting = false;
5726            this._paint();
5727        }
5728
5729        _sync() {
5730            this._reserveLabels();
5731            this._write(this.valueMin, this.valueMax);
5732        }
5733
5734        //Widest value this slider can ever show, so live values during a drag can never
5735        //resize the field and pull the node out from under the pointer. Counted in digit
5736        //widths, with the separator and sign costing well under a digit each.
5737        _reserveLabels() {
5738            const decimals = this.step ? Math.max(this._decimals(this.getAttribute("step")), this._decimals(this.getAttribute("min"))) : 1;
5739            const digits = Math.max(String(Math.floor(Math.abs(this.min))).length, String(Math.floor(Math.abs(this.max))).length);
5740            const width = digits + (decimals ? decimals + 0.5 : 0) + (this.min < 0 ? 0.6 : 0);
5741            this.style.setProperty("--_labelChars", Math.round(width * 10) / 10);
5742        }
5743
5744        _decimals(raw) {
5745            const text = String(raw);
5746            const dot = text.indexOf(".");
5747            return dot === -1 ? 0 : text.length - dot - 1;
5748        }
5749
5750        _paint() {
5751            const low = this.valueMin;
5752            const high = this.valueMax;
5753            this.style.setProperty("--_min", this._fractionOf(low));
5754            this.style.setProperty("--_max", this._fractionOf(high));
5755            this._readout("min", low);
5756            this._readout("max", high);
5757        }
5758
5759        //Field plus ARIA for one node. Used both by _paint and live during a drag.
5760        //The field's own bounds are the other node's position, so native validation
5761        //agrees with the clamping applied on blur.
5762        _readout(which, value) {
5763            const low = which === "min" ? this.min : this.valueMin;
5764            const high = which === "min" ? this.valueMax : this.max;
5765            const input = this._inputs[which];
5766            //Never rewrite a field that is being typed into: a half-typed "0." parses to
5767            //0, and echoing that back would delete the separator from under the caret.
5768            if (input !== document.activeElement) {
5769                input.value = this._format(value);
5770            }
5771            input.min = String(low);
5772            input.max = String(high);
5773            input.step = this.step ? String(this.step) : "any";
5774            const node = this._nodes[which];
5775            node.setAttribute("aria-valuenow", String(value));
5776            node.setAttribute("aria-valuemin", String(low));
5777            node.setAttribute("aria-valuemax", String(high));
5778            node.setAttribute("aria-valuetext", this._format(value));
5779        }
5780
5781        //Typing repositions the node straight away and submits once typing settles.
5782        _onFieldInput(which) {
5783            this._scheduleCommit(which);
5784            const typed = parseFloat(this._inputs[which].value);
5785            if (isNaN(typed)) {
5786                return;
5787            }
5788            const bounded = which === "min" ? Math.min(this._limit(typed), this.valueMax) : Math.max(this._limit(typed), this.valueMin);
5789            this.style.setProperty(which === "min" ? "--_min" : "--_max", this._fractionOf(this._snap(bounded)));
5790        }
5791
5792        _scheduleCommit(which) {
5793            clearTimeout(this._commitTimer);
5794            this._commitTimer = setTimeout(() => this._commitField(which, false), 200);
5795        }
5796
5797        _onFieldKeyDown(event, which) {
5798            if (event.key === "Enter") {
5799                event.preventDefault();
5800                this._commitField(which, true);
5801            } else if (event.key === "Escape") {
5802                event.preventDefault();
5803                clearTimeout(this._commitTimer);
5804                this._paint();
5805                this._fill(which);
5806            } else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
5807                event.preventDefault();
5808                this._nudgeField(which, event.key === "ArrowUp" ? 1 : -1);
5809            }
5810        }
5811
5812        //Stepped from the field's own value. Native stepping aligns to the field's min,
5813        //which is the other node, so 50 against a min of 27.5 would step to 49.5 and
5814        //could never get back to 50.
5815        _nudgeField(which, direction) {
5816            const input = this._inputs[which];
5817            const current = this._parse(input.value, which === "min" ? this.valueMin : this.valueMax);
5818            const stepped = current + direction * (this.step || 1);
5819            const bounded = which === "min" ? Math.min(this._limit(stepped), this.valueMax) : Math.max(this._limit(stepped), this.valueMin);
5820            input.value = this._format(this._snap(bounded));
5821            this._onFieldInput(which);
5822        }
5823
5824        _fill(which) {
5825            this._inputs[which].value = this._format(which === "min" ? this.valueMin : this.valueMax);
5826        }
5827
5828        //Validates a typed value: unusable input falls back to the value already
5829        //committed, and anything out of range lands on the nearest valid value. Only an
5830        //explicit commit rewrites the field, so a debounced one leaves the caret alone.
5831        _commitField(which, normalize) {
5832            clearTimeout(this._commitTimer);
5833            const typed = parseFloat(this._inputs[which].value);
5834            const before = [this.valueMin, this.valueMax];
5835            this._move(which, isNaN(typed) ? before[which === "min" ? 0 : 1] : typed);
5836            if (normalize) {
5837                this._fill(which);
5838            }
5839            if (before[0] !== this.valueMin || before[1] !== this.valueMax) {
5840                this._notify();
5841            }
5842        }
5843
5844        //Stepped values are shown exactly; a continuous slider is rounded for display.
5845        _format(value) {
5846            return String(parseFloat(value.toFixed(this.step ? 10 : 1)));
5847        }
5848
5849        _fractionOf(value) {
5850            return (value - this.min) / (this.max - this.min || 1);
5851        }
5852
5853        _valueOf(fraction) {
5854            return this.min + fraction * (this.max - this.min);
5855        }
5856
5857        //Geometry is read from what was cached at pointerdown, so a drag can never be
5858        //disturbed by the labels resizing the track underneath it.
5859        _geometry() {
5860            const rect = this._rect || this._track.getBoundingClientRect();
5861            const size = this._size || this._nodes.min.getBoundingClientRect().width;
5862            return { left: rect.left, size: size, travel: Math.max(0, rect.width - size) };
5863        }
5864
5865        //Both centres travel the track minus one node, with the max node's centre a full
5866        //node further along. So each centre reaches its end of the track, and equal values
5867        //leave the two nodes exactly touching instead of overlapping.
5868        _centerOf(which, fraction) {
5869            const geometry = this._geometry();
5870            return fraction * geometry.travel + (which === "max" ? geometry.size : 0);
5871        }
5872
5873        _fractionAt(which, clientX) {
5874            const geometry = this._geometry();
5875            if (!geometry.travel) {
5876                return 0;
5877            }
5878            const offset = clientX - geometry.left - (which === "max" ? geometry.size : 0);
5879            return Math.min(1, Math.max(0, offset / geometry.travel));
5880        }
5881
5882        //A press anywhere on the track hands control to whichever centre is nearest.
5883        _closestNode(clientX, target) {
5884            const node = target && target.closest ? target.closest(".sliderNode") : null;
5885            if (node) {
5886                return node.classList.contains("max") ? "max" : "min";
5887            }
5888            const x = clientX - this._geometry().left;
5889            const toMin = Math.abs(x - this._centerOf("min", this._fractionOf(this.valueMin)));
5890            const toMax = Math.abs(x - this._centerOf("max", this._fractionOf(this.valueMax)));
5891            return toMin <= toMax ? "min" : "max";
5892        }
5893
5894        _move(which, value) {
5895            if (which === "min") {
5896                this._write(Math.min(value, this.valueMax), this.valueMax);
5897            } else {
5898                this._write(this.valueMin, Math.max(value, this.valueMin));
5899            }
5900        }
5901
5902        //Moves and reads out the node without writing a value: the label shows what
5903        //would be committed if released here, but nothing is stored or submitted yet.
5904        _drag(clientX) {
5905            let fraction = this._fractionAt(this._activeNode, clientX);
5906            if (this._activeNode === "min") {
5907                fraction = Math.min(fraction, this._fractionOf(this.valueMax));
5908                this.style.setProperty("--_min", fraction);
5909            } else {
5910                fraction = Math.max(fraction, this._fractionOf(this.valueMin));
5911                this.style.setProperty("--_max", fraction);
5912            }
5913            this._dragFraction = fraction;
5914            this._readout(this._activeNode, this._snap(this._valueOf(fraction)));
5915        }
5916
5917        _onPointerDown(event) {
5918            if (event.button > 0) {
5919                return;
5920            }
5921            this._rect = this._track.getBoundingClientRect();
5922            this._size = this._nodes.min.getBoundingClientRect().width;
5923            this._activeNode = this._closestNode(event.clientX, event.target);
5924            this._dragStart = [this.valueMin, this.valueMax];
5925            this._nodes[this._activeNode].focus({ preventScroll: true });
5926            this._track.setPointerCapture(event.pointerId);
5927            this.setAttribute("dragging", "");
5928            this._drag(event.clientX);
5929            event.preventDefault();
5930        }
5931
5932        _onPointerMove(event) {
5933            if (!this._activeNode) {
5934                return;
5935            }
5936            this._drag(event.clientX);
5937        }
5938
5939        //Release is the only point where the drag turns into a value.
5940        _onPointerUp(event) {
5941            if (!this._activeNode) {
5942                return;
5943            }
5944            const which = this._activeNode;
5945            const value = this._valueOf(this._dragFraction);
5946            this._activeNode = null;
5947            this._rect = null;
5948            this._size = null;
5949            this.removeAttribute("dragging");
5950            this._move(which, value);
5951            if (this._dragStart[0] !== this.valueMin || this._dragStart[1] !== this.valueMax) {
5952                this._notify();
5953            }
5954        }
5955
5956        //An interrupted drag never became a value, so just put the node back.
5957        _onPointerCancel() {
5958            if (!this._activeNode) {
5959                return;
5960            }
5961            this._activeNode = null;
5962            this._rect = null;
5963            this._size = null;
5964            this.removeAttribute("dragging");
5965            this._paint();
5966        }
5967
5968        _onKeyDown(event) {
5969            const node = event.target.closest ? event.target.closest(".sliderNode") : null;
5970            if (!node) {
5971                return;
5972            }
5973            const which = node.classList.contains("max") ? "max" : "min";
5974            const current = which === "max" ? this.valueMax : this.valueMin;
5975            let value = current;
5976            switch (event.key) {
5977                case "ArrowLeft":
5978                case "ArrowDown":
5979                    value = current - this.nudge;
5980                    break;
5981                case "ArrowRight":
5982                case "ArrowUp":
5983                    value = current + this.nudge;
5984                    break;
5985                case "Home":
5986                    value = this.min;
5987                    break;
5988                case "End":
5989                    value = this.max;
5990                    break;
5991                default:
5992                    return;
5993            }
5994            event.preventDefault();
5995            const before = [this.valueMin, this.valueMax];
5996            this._move(which, value);
5997            if (before[0] !== this.valueMin || before[1] !== this.valueMax) {
5998                this._notify();
5999            }
6000        }
6001
6002        _notify() {
6003            this.dispatchEvent(new CustomEvent("change", { bubbles: true }));
6004        }
6005    },
6006);
6007
6008// Empty callbacks
6009function onNavbarCreated() {}
6010function onProductAdded() {}
6011function onProductRemoved() {}
6012function onOrderCompleted() {}
6013function onBasketParsed(data) {}
6014function onBasketUpdated() {}
6015function onScroll(scroll) {}
6016function onWindowResize(width) {}
6017function onPageInit(page) {}

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.