PageSourceSearch

https://amapizza.pt/assets/js/function.js

js amapizza.pt collected 2026-10-02 01:08:16 UTC 44,906 bytes, 1,416 lines download raw bytes

1
2//---------------------------------------
3
4// Transition Block Next
5function transitionToNextBlock(nextBlockID){
6    let activeBlock = $('.block-active');
7    let nextBlock = $('#' + nextBlockID);
8    activeBlock.find('.block-content')
9        .removeClass("opacity-100 translate-y-0 scale-100 mr-0")
10        .addClass("opacity-0 translate-y-4 scale-95 -ml-10");
11    activeBlock.removeClass('block-active');
12    setTimeout(() => {
13        activeBlock.addClass("hidden");
14        nextBlock.addClass('block-active').removeClass("hidden");
15        setTimeout(() => {
16            nextBlock.find('.block-content')
17            .removeClass("opacity-0 translate-y-4 scale-95 -mr-10 -ml-10")
18            .addClass("opacity-100 translate-y-0 scale-100 mr-0 ml-0");
19        }, 100);
20    }, 100);
21}
22
23// Transition Block Prev
24function transitionToPrevBlock(prevBlockID){
25    let activeBlock = $('.block-active');
26    let prevBlock = $('#' + prevBlockID);
27    activeBlock.find('.block-content')
28    .removeClass("opacity-100 translate-y-0 scale-100 mr-0")
29    .addClass("opacity-0 translate-y-4 scale-95 -ml-10");
30    activeBlock.removeClass('block-active');
31    setTimeout(() => {
32        activeBlock.addClass("hidden");
33        prevBlock.removeClass('hidden');
34        setTimeout(() => {
35            prevBlock.find('.block-content')
36            .removeClass("opacity-0 translate-y-4 scale-95 -mr-10 -ml-10")
37            .addClass("opacity-100 translate-y-0 scale-100 mr-0 ml-0"); 
38        }, 100);
39    }, 100);
40    prevBlock.addClass('block-active');
41}
42
43
44function feedbackSuccess(message=false){
45    var image = $(".feedback-success-gif");
46    image.attr("src", "");
47    showModal("feedback-success")
48    if(message != false){
49      $(".feedback-message-success").text(message);
50    }
51    image.attr("src", "/assets/img/gif/basket.gif");
52    setTimeout(() => {
53        closeModal("feedback-success")   
54    }, 1600);
55}
56
57function feedbackOffer(message=false,img){
58    var image = $(".offer-success-gif");
59    showModal("offer-success")
60    if(message != false){
61      $(".offer-message-success").text(message);
62    }
63    image.attr("src", img);
64    showConfetti()
65}
66
67
68// Show Modal
69
70function showModal(modalId) {
71    $("#"+modalId).removeClass("hidden");
72    setTimeout(() => {
73        $("#"+modalId + " .modal-backdrop").removeClass("opacity-0").addClass("opacity-100");
74        $("#"+modalId + " .modal-content").removeClass("opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95").addClass("opacity-100 translate-y-0 sm:scale-100");
75    }, 10);
76    
77}
78
79// Hide Modal
80function closeModal(modalId) {
81    $("#"+modalId + " .modal-backdrop").removeClass("opacity-100").addClass("opacity-0");
82    $("#"+modalId + " .modal-content").removeClass("opacity-100 translate-y-0 sm:scale-100").addClass("opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95");
83    setTimeout(() => {
84        $("#"+modalId).addClass("hidden");
85    }, 500);
86}
87function trackProductView(productId) {
88    const modalId = `popup-${productId}`;
89    const $modal = $("#" + modalId);
90
91    if (!$modal.length) {
92        console.warn(`[Pixel] Modal não encontrada para produto: ${productId}`);
93        return;
94    }
95
96    const productName = $modal.find("div.title")
97        .clone().children().remove().end().text().trim();
98
99    let productPrice = 0;
100    const $priceSpan = $modal.find(`.total-product-uuid-${productId}`);
101    if ($priceSpan.length) {
102        const discount = parseFloat($priceSpan.data("product-discount") || 0);
103        const total = parseFloat($priceSpan.data("product-total") || 0);
104        productPrice = total - discount;
105    }
106
107    // Criar objeto comum
108    const product = {
109        id: productId,
110        name: productName,
111        price: productPrice
112    };
113
114    // --- Meta Pixel ---
115    if (typeof fbq === "function" && window.fbPixelIsActive === true) {
116    fbq('track', 'ViewContent', {
117
118            content_ids: [product.id],
119            content_name: product.name,
120            content_type: 'product',
121            value: product.price,
122            currency: 'EUR'
123        });
124    }
125
126    // --- Google Analytics 4 ---
127    if (typeof gtag === "function") {
128        gtag("event", "view_item", {
129            currency: "EUR",
130            value: product.price,
131            items: [
132                {
133                    item_id: product.id,
134                    item_name: product.name,
135                    quantity: 1,
136                    price: product.price
137                }
138            ]
139        });
140    }
141
142    // --- Google Tag Manager ---
143        // --- Google Tag Manager ---
144    if (typeof dataLayer !== 'undefined') {
145        setTimeout(() => {
146            dataLayer.push({
147                event: "view_item",
148                gtmparams: {
149                    pagetype: "product",
150                    product: {
151                        id: product.id,
152                        name: product.name
153                    }
154                },
155                ecommerce: {
156                    currency: "EUR",
157                    value: product.price,
158                    items: [
159                        {
160                            item_id: product.id,
161                            item_name: product.name,
162                            quantity: 1,
163                            price: product.price
164                        }
165                    ]
166                }
167            });
168
169            if (window.debugPixel === true) {
170                console.log("[GTM] view_item:", {
171                    event: "view_item",
172                    value: product.price,
173                    currency: "EUR",
174                    items: [{
175                        item_id: product.id,
176                        item_name: product.name,
177                        quantity: 1,
178                        price: product.price
179                    }]
180                });
181            }
182        }, 200); // pequeno atraso para garantir GTM ativo
183    }
184
185
186    // --- Debug ---
187    if (window.debugPixel === true) {
188        console.log("[Meta Pixel] ViewContent:", product);
189        console.log("[GA4] view_item:", product);
190        console.log("[GTM] view_item:", {
191            event: "view_item",
192            currency: "EUR",
193            value: product.price,
194            items: [{
195                item_id: product.id,
196                item_name: product.name,
197                quantity: 1,
198                price: product.price
199            }]
200        });
201    }
202}
203
204
205
206function trackInitiateCheckout() {
207    const cart = JSON.parse(localStorage.getItem('cart')) || [];
208
209    if (!cart.length) {
210        console.warn("[Meta Pixel] Carrinho vazio no InitiateCheckout.");
211        return;
212    }
213
214    let totalValue = 0;
215    const contents = cart.map(item => {
216        const value = parseFloat(item.price || 0);
217        totalValue += value * item.qty;
218
219        return {
220            id: item.id,
221            quantity: item.qty,
222            item_price: value,
223            name: item.name || "",
224            price: value
225        };
226    });
227
228    // --- Meta Pixel ---
229    if (typeof fbq === "function") {
230        fbq('track', 'InitiateCheckout', {
231            contents: contents.map(item => ({
232                id: item.id,
233                quantity: item.quantity,
234                item_price: item.price
235            })),
236            content_type: 'product',
237            value: totalValue,
238            currency: 'EUR'
239        });
240    }
241
242    // --- Google Analytics 4 (direto) ---
243    if (typeof gtag === "function") {
244        gtag("event", "begin_checkout", {
245            currency: "EUR",
246            value: totalValue,
247            items: contents.map(item => ({
248                item_id: item.id,
249                item_name: item.name,
250                quantity: item.quantity,
251                price: item.price
252            }))
253        });
254    }
255
256    // --- Google Tag Manager ---
257    if (typeof dataLayer !== "undefined") {
258        setTimeout(() => {
259            dataLayer.push({
260                event: "begin_checkout",
261                ecommerce: {
262                    currency: "EUR",
263                    value: totalValue,
264                    items: contents.map(item => ({
265                        item_id: item.id,
266                        item_name: item.name,
267                        quantity: item.quantity,
268                        price: item.price
269                    }))
270                }
271            });
272
273            if (window.debugPixel === true) {
274                console.log("[GTM] begin_checkout", {
275                    currency: "EUR",
276                    value: totalValue,
277                    items: contents.map(item => ({
278                        item_id: item.id,
279                        item_name: item.name,
280                        quantity: item.quantity,
281                        price: item.price
282                    }))
283                });
284            }
285        }, 150); // pequeno delay para GTM estar pronto
286    }
287
288    if (window.debugPixel === true) {
289        console.log("[Meta Pixel] InitiateCheckout:", {
290            contents,
291            value: totalValue
292        });
293        console.log("[GA4] begin_checkout:", {
294            currency: "EUR",
295            value: totalValue,
296            items: contents
297        });
298    }
299}
300
301
302
303
304
305
306
307
308// Toast
309function toast(type,message,duration) {
310    // Choosing Color of Toast
311    if(type == "error"){color = "#dc2626"}else if(type == "warning"){color = "#f97316"}else if(type == "info"){color = "#0284c7"}else{color = "#4d7c0f"}
312    Toastify({
313      text: message,
314      duration: duration, 
315      close: true, 
316      gravity: "top",
317      backgroundColor: color, 
318      className: "custom-toast", 
319    }).showToast();
320}
321
322
323
324
325
326
327
328function updateData(element, type) {
329    var id = $(element).attr('id');
330    var value;
331    if(type == 'checkbox') {
332        value = $(element).prop('checked');
333    } else if(type == 'select') {
334       
335        value = $(element).select2('data').map(function(item) { return item.id; });
336
337    } else if(type == 'tinymce') {
338        value = element.getContent(); // for TinyMCE, element is already the value
339    } else {
340        value = $(element).val();
341    }   
342    var textAreaValue = $('#data').val();
343    var jsonData = textAreaValue ? JSON.parse(textAreaValue) : {};
344    jsonData[id] = value;
345    $('#data').val(JSON.stringify(jsonData));
346}
347
348
349
350function thumbnailRemoveFile(file,element,field){
351    let dataArray = JSON.parse($("#"+field).val());
352    for (let i = dataArray.length - 1; i >= 0; i--) {
353        if (dataArray[i].url.original == file) {
354            dataArray.splice(i, 1);
355        }
356    }
357    $("#"+field).val(JSON.stringify(dataArray)).trigger('change');
358    $("#"+element).remove();
359}
360
361
362async function saveData(name, value) {
363    return new Promise((resolve, reject) => {
364        $.ajax({
365            url: '/common/set/data',
366            method: 'POST',
367            data: {
368                key: name,
369                value: value
370            },
371            headers: {
372                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') // Include CSRF token for Laravel
373            },
374            success: function(response) {
375                if (response?.success === true) {
376                    console.log(`Saved: ${name} : ${value}`);
377                    resolve(true);
378                } else {
379                    console.log(`Failed to save data with name: ${name}`);
380                    resolve(false);
381                }
382            },
383            error: function(error) {
384                console.log(`Error occurred while saving data with name: ${name}`, error);
385                reject(error); 
386            }
387        }).fail(function(jqXHR, textStatus, errorThrown) {
388            if (jqXHR.status == 419) { 
389                window.location.reload();
390            }
391        });
392    });
393}
394
395async function saveDataBatch(entries) {
396    return new Promise((resolve, reject) => {
397        $.ajax({
398            url: '/common/set/data-batch',
399            method: 'POST',
400            data: {
401                entries: entries
402            },
403            headers: {
404                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
405            },
406            success: function(response) {
407                if (response?.success === true) {
408                    resolve(true);
409                } else {
410                    resolve(false);
411                }
412            },
413            error: function(error) {
414                console.log('Error occurred while saving batch data', error);
415                reject(error);
416            }
417        }).fail(function(jqXHR) {
418            if (jqXHR.status == 419) {
419                window.location.reload();
420            }
421        });
422    });
423}
424
425// Create UniqueId
426function createUniqueId() {
427    return Date.now().toString(36) + Math.random().toString(36).substring(2);
428}
429
430function generateUniqueId() {
431    return 'xxxx-xxxx-4xxx-yxxx-xxxxxxx'.replace(/[xy]/g, function (c) {
432       var r = Math.random() * 16 | 0,
433          v = c == 'x' ? r : (r & 0x3 | 0x8);
434       return v.toString(16);
435    });
436 }
437
438
439async function migrateData() {
440    // Cart
441    await saveData("cart", localStorage.getItem('cart'));
442    // Information
443    await saveData("detail", localStorage.getItem('checkout-information'));
444    // Upsell
445    await saveData("upsell", localStorage.getItem('upsell'));
446    // Notes
447    await saveData("note", localStorage.getItem('notes'));
448}
449
450
451
452function resetOrderCookies(){
453    localStorage.removeItem('cart');
454    localStorage.removeItem('checkout-information');
455}
456
457function loading(){
458    $("#page-loader").removeClass("hidden");
459}
460
461function loaded(){
462    $("#page-loader").addClass("hidden");
463}
464
465
466function loadRedirect(url,duration){
467    loading();
468    setTimeout(() => {
469        app.view.main.router.navigate(url)
470        //window.location.href=url;
471    }, duration);
472}
473
474function printArea(contentId, title) {
475    // Create a hidden iframe for printing purposes
476    const printIframe = $('<iframe>', {
477        'id': 'printIframe',
478        'style': 'position: absolute; top: -9999px; left: -9999px;'
479    });
480
481    // Append the iframe to the body
482    $('body').append(printIframe);
483
484    // Access the document of the iframe
485    const iframeDoc = printIframe[0].contentDocument || printIframe[0].contentWindow.document;
486
487    // Prepare the content for the iframe
488    iframeDoc.open();
489    iframeDoc.write('<html><head><title>' + title + '</title></head><body>');
490    iframeDoc.write($('#' + contentId).html());
491    iframeDoc.write('</body></html>');
492    iframeDoc.close();
493
494    // Ensure the content is loaded before initiating the print
495    $(iframeDoc).ready(function() {
496        printIframe[0].contentWindow.focus();
497        printIframe[0].contentWindow.print();
498
499        // Remove the iframe after printing
500        setTimeout(() => {
501            printIframe.remove();
502        }, 1000);
503    });
504}
505
506//
507async function proceedToCheckout() {
508    loading();
509    const cartData = localStorage.getItem('cart');
510    const isSaved = await saveData("cart", cartData);
511    if (isSaved) {
512        setTimeout(() => {
513            window.location.href = '/checkout';
514        }, 1000);
515       
516    } else {
517        console.log("Error redirecting to checkout")
518    }
519}
520
521//
522function backToStore() {
523    cart = getCart();
524    if (cart?.length > 0) {
525      var first_item = cart[0];
526      app.view.main.router.navigate(first_item?.store_url)
527    }
528}
529  
530
531
532async function migrateCheckoutData(context = null) {
533    const $context = context ? $(context) : null;
534    const previousCheckoutUrl = $context && $context.length
535        ? ($context.data("url") || window.location.pathname)
536        : window.location.pathname;
537
538    const entries = {
539        previousCheckout: previousCheckoutUrl,
540        cart: localStorage.getItem('cart'),
541        detail: localStorage.getItem('checkout-information'),
542        upsell: localStorage.getItem('upsell'),
543        note: localStorage.getItem('notes')
544    };
545
546    const orderMode = localStorage.getItem('order_mode');
547    if (orderMode) {
548        entries.order_mode = orderMode;
549    }
550
551    const orderTime = localStorage.getItem('order_time');
552    if (orderTime) {
553        entries.order_time = orderTime;
554    }
555
556    const address = localStorage.getItem('sqv_address');
557    if (address) {
558        entries.address = address;
559    }
560
561    try {
562        return await saveDataBatch(entries);
563    } catch (error) {
564        console.log('Checkout migration failed', error);
565        return false;
566    }
567}
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584function showSuggestions(inputId, results, provider) {
585    const suggestionDiv = $('#autocomplete-suggestions');
586    suggestionDiv.empty();
587
588    results.forEach(result => {
589        const address = result.formatted_address;
590        const country = result.country || ''; // Assuming each result has a 'country' field
591        const lat = result.lat;
592        const lng = result.lng;
593        const suggestionEl = $('<div>').addClass('px-3 py-2 hover:bg-gray-200 cursor-pointer border-b border-gray-200');
594        const addressEl = $('<div>').html('<div class="truncate"> <i class="bi bi-geo-alt-fill"></i> '+address+'</div>');
595        const countryEl = $('<div>').addClass('pl-5 text-sm text-gray-500').text(country);
596
597        suggestionEl.append(addressEl);
598
599        suggestionEl.click(function() {
600            if(provider === 'google' && result.place_id) {
601                getGooglePlaceDetails(result.place_id, function(details) {
602                    // Use details to populate fields
603                    // details = { address, lat, lng, street, city, state, country, zip }
604                    handleSelectedPlace(details, inputId);
605                });
606            } else {
607                handleSelectedPlace(result, inputId);
608            }
609        });
610
611            // // Send AJAX request to Laravel route to set the cookie
612            // saveData('address',JSON.stringify(addressData),false);
613        suggestionDiv.append(suggestionEl);
614    });
615}
616
617
618function handleSelectedPlace(details, inputId) {
619    // Here you handle the selected place, populate inputs, etc.
620    // details = { address, lat, lng, street, city, state, country, zip }
621    // ...
622
623    console.log(details);
624    let addressData = { 
625        address: details.address,
626        lat: details.lat,
627        lng: details.lng,
628        street: details.street,
629        street_number: details.street_number,
630        city: details.city,
631        state: details.state,
632        country: details.country,
633        zip: details.zip,
634        // ...
635    };
636
637    $('#address-street').val(details.street);
638    $('#address-street-number').val(details.street_number);
639    $('#address-lat').val(details.lat);
640    $('#address-lng').val(details.lng);
641    $('#address-city').val(details.city);
642    $('#address-state').val(details.state);
643    $('#address-country').val(details.country);
644    $('#address-zip').val(details.zip);
645    // $('#details-input').val(); 
646
647    // Do whatever you want with the selected place, for example:
648    $(`#${inputId}`).val(details.street);
649
650    app.popup.open('.modal-address');
651    
652  
653    // Or populate additional inputs, etc.
654}
655
656
657function getGooglePlaceDetails(placeId, callback) {
658    const service = new google.maps.places.PlacesService(document.createElement('div'));
659    service.getDetails({ placeId: placeId }, function(place, status) {
660        if (status === google.maps.places.PlacesServiceStatus.OK) {
661            const addressComponents = {};
662            place.address_components.forEach(component => {
663                if(component.types[0]) {
664                    addressComponents[component.types[0]] = component.long_name;
665                }
666            });
667            const details = {
668                address: place.formatted_address,
669                lat: place.geometry.location.lat(),
670                lng: place.geometry.location.lng(),
671                street: addressComponents.route || "",
672                street_number: addressComponents.street_number || "",
673                city: addressComponents.locality || "",
674                state: addressComponents.administrative_area_level_1 || "",
675                country: addressComponents.country || "",
676                zip: addressComponents.postal_code || "",
677            };
678            callback(details);
679        }
680    });
681}
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700  function dataExpand(item){
701    $('#expand-' + item).removeClass('hidden');
702    $('#setting-' + item).addClass('hidden'); 
703    setTimeout(() => {
704      $('#expand-' + item+' .loading').addClass('hidden');
705      $('#expand-' + item+' .content').removeClass('hidden');
706      $('#input-' + item).focus();
707    }, 1000);
708}
709
710function closeExpand(item){
711    $('#expand-' + item).addClass('hidden');
712    $('#setting-' + item).removeClass('hidden');
713    $('#expand-' + item+' .loading').removeClass('hidden');
714    $('#expand-' + item+' .content').addClass('hidden');
715}
716
717
718
719function acceptCookies() {
720    Cookies.set('cookieConsent', 'true', {
721        expires: 365
722    });
723    closeCookies();
724}
725function rejectCookies() {
726    Cookies.set('cookieConsent', 'true', {
727        expires: 365
728    });
729    closeCookies();
730}
731function closeCookies() {
732    $('#cookie-banner, #cookie-consent-modal').addClass('hidden');
733}
734
735
736function showMap(id, lat, lng, locations, card=false, zoom) {
737    var map = new google.maps.Map(document.getElementById(id), {
738        center: {lat: lat, lng: lng},
739        zoom: zoom,
740        scrollwheel: false,
741        gestureHandling: 'cooperative',
742        streetViewControl: false,
743        mapTypeControl: false,
744        fullscreenControl: false
745    });
746
747    // Cria um InfoWindow fora do loop para que possa ser reutilizado
748    var infowindow = new google.maps.InfoWindow();
749
750    var defaultImageUrl = $('.logo-light').attr('src');
751
752    // Função para criar e abrir a InfoWindow
753    function createInfoWindow(marker, place) {
754        var imageUrl = (place && place.mapa_logo) ? place.mapa_logo : defaultImageUrl;
755        var content = '<div style="">' +
756            '<a href="/contact" class="external cursor-pointer text-white hover-text-primary duration-200">' +
757            '<img src="'+imageUrl+'" class="lg:mr-3 lg:pb-3" style="width:120px"></a></div>';
758
759        infowindow.setContent(content);
760        infowindow.open(map, marker);
761    }
762
763    locations.forEach(function(place) {
764        var marker = new google.maps.Marker({
765            position: {lat: Number(place.lat), lng: Number(place.lng)},
766            map: map,
767            title: place.name
768        });
769
770        // Cria e abre a InfoWindow para cada marcador imediatamente
771        createInfoWindow(marker, place);
772    });
773
774    // Listener para fechar a InfoWindow quando o mapa é clicado
775    google.maps.event.addListener(map, 'click', function() {
776        infowindow.close();
777    });
778}
779
780function escapeHTML(unsafeText) {
781    let text = String(unsafeText);
782    return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
783               .replace(/"/g, "&quot;").replace(/'/g, "&#039;");
784}
785
786
787async function submitAddress(location,lat,lng,country){
788    var address = {
789            address: location,
790            lat: lat,
791            lng: lng,
792            country: country,
793        };
794        // Stringify Object
795        const isSaved = await saveData("address",JSON.stringify(address));
796        if (isSaved) {
797            window.location.href = '/searchresults';
798        } else {
799            console.log("Error redirecting to search")
800            window.location.href = '';
801        }
802}
803
804  // Smooth scrolling function
805 function smoothScrollToElement(element,duration=400,top=0) {
806    const $element = $(element),
807          $scrollableParent = $element.parents(".page-content, .panel, .card-expandable .card-content").eq(0);
808    if (!$scrollableParent.length) return false;
809    // Calculate the position to scroll to
810    const elementOffset = $element.offset().top - top - $scrollableParent.offset().top + $scrollableParent.scrollTop();
811    // Scroll to the element's top position smoothly
812    $scrollableParent.animate({ scrollTop: elementOffset }, duration);
813    return true;
814}
815
816
817
818
819
820
821function elementShake(element,focus=null,duration=380,distance=7){
822    $(element).effect("shake", {
823        times: 4,
824        distance: distance,
825        duration: duration
826    });
827    if(focus){
828        $(focus).focus();
829        $(focus).addClass("border-red-500").removeClass("border-gray-400");
830        setTimeout(() => {
831            $(focus).removeClass("border-red-500").addClass("border-gray-400");
832        }, duration);
833    }
834    $(element).addClass("border-red-500").removeClass("border-gray-400");
835    setTimeout(() => {
836        $(element).removeClass("border-red-500").addClass("border-gray-400");
837    }, duration);
838}
839
840
841
842function openAuthModal() {
843    setTimeout(() => {
844        $("#open-auth").click();
845        // $(".close-auth").addClass("hidden");
846    }, 200);
847}
848
849function toggleAppMode(isAppMode) {
850    if (isAppMode) {
851        localStorage.setItem("app_mode", "true");
852    } else {
853        localStorage.removeItem("app_mode");
854    }
855}
856
857function getAppContextParams() {
858    const searchParams = new URLSearchParams(window.location.search);
859    const isAppFromUrl = searchParams.get("app") === "true";
860    const isAppMode = localStorage.getItem("app_mode") === "true";
861
862    if (!isAppFromUrl && !isAppMode) {
863        return null;
864    }
865
866    const device = searchParams.get("device") || localStorage.getItem("device") || "";
867    const tokenFromUrl = searchParams.get("fcm_token") || searchParams.get("token") || "";
868    const tokenFromStorage = localStorage.getItem("sqv_fcm_token") || localStorage.getItem("app_token") || "";
869    const token = tokenFromUrl || tokenFromStorage;
870
871    const tokenKeyFromUrl = searchParams.has("fcm_token") ? "fcm_token" : (searchParams.has("token") ? "token" : null);
872    const tokenKey = tokenKeyFromUrl || localStorage.getItem("app_token_key") || "fcm_token";
873
874    if (device) {
875        localStorage.setItem("device", device);
876    }
877    if (token) {
878        localStorage.setItem("sqv_fcm_token", token);
879        localStorage.setItem("app_token", token);
880    }
881    localStorage.setItem("app_token_key", tokenKey);
882
883    return {
884        app: "true",
885        device,
886        token,
887        tokenKey
888    };
889}
890
891function shouldPreserveAppParams(targetUrl) {
892    if (!targetUrl) return false;
893
894    const href = String(targetUrl).trim();
895    if (!href || href.startsWith("#") || href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:")) {
896        return false;
897    }
898
899    let url;
900    try {
901        url = new URL(href, window.location.origin);
902    } catch (e) {
903        return false;
904    }
905
906    if (!["http:", "https:"].includes(url.protocol)) {
907        return false;
908    }
909
910    if (url.searchParams.get("app") === "false") {
911        return false;
912    }
913
914    const hostname = url.hostname || "";
915    const sameOrigin = url.origin === window.location.origin;
916    const isSqvDomain = hostname.endsWith("sistemaquevende.com");
917
918    return sameOrigin || isSqvDomain;
919}
920
921function appendAppContextToUrl(targetUrl) {
922    const context = getAppContextParams();
923    if (!context || !shouldPreserveAppParams(targetUrl)) {
924        return targetUrl;
925    }
926
927    let url;
928    try {
929        url = new URL(targetUrl, window.location.origin);
930    } catch (e) {
931        return targetUrl;
932    }
933
934    url.searchParams.set("app", "true");
935
936    if (context.device) {
937        url.searchParams.set("device", context.device);
938    }
939
940    if (context.token) {
941        url.searchParams.delete("token");
942        url.searchParams.delete("fcm_token");
943        url.searchParams.set(context.tokenKey, context.token);
944    }
945
946    const isAbsoluteInput = /^https?:\/\//i.test(String(targetUrl));
947    if (isAbsoluteInput) {
948        return url.toString();
949    }
950
951    return url.pathname + url.search + url.hash;
952}
953
954function preserveAppContextAcrossNavigation() {
955    const context = getAppContextParams();
956    if (!context) {
957        return;
958    }
959
960    const currentUrl = appendAppContextToUrl(window.location.href);
961    if (currentUrl && currentUrl !== window.location.href) {
962        window.history.replaceState({}, "", currentUrl);
963    }
964
965    document.querySelectorAll("a[href]").forEach((anchor) => {
966        const originalHref = anchor.getAttribute("href");
967        const nextHref = appendAppContextToUrl(originalHref);
968        if (nextHref && nextHref !== originalHref) {
969            anchor.setAttribute("href", nextHref);
970        }
971    });
972
973    document.querySelectorAll("form[action]").forEach((form) => {
974        const originalAction = form.getAttribute("action");
975        const nextAction = appendAppContextToUrl(originalAction);
976        if (nextAction && nextAction !== originalAction) {
977            form.setAttribute("action", nextAction);
978        }
979    });
980
981    document.addEventListener("click", function (event) {
982        const anchor = event.target.closest("a[href]");
983        if (!anchor) return;
984
985        const href = anchor.getAttribute("href");
986        const nextHref = appendAppContextToUrl(href);
987        if (nextHref && nextHref !== href) {
988            anchor.setAttribute("href", nextHref);
989        }
990    });
991}
992
993function checkAppModeVisibility() {
994    if (localStorage.getItem("app_mode") == "true") {
995        $(".hidden-app").css("display", "none");
996        $(".anti-hidden-app").css("display", "block");
997    } else {
998        $(".hidden-app").css("display", "block");
999        $(".anti-hidden-app").css("display", "none");
1000    }
1001}
1002
1003function redirectToSearchIfAuthenticated() {
1004    if (localStorage.getItem("app_mode") == "true" && isAuth()) {
1005        window.location.href = appendAppContextToUrl("/searchresults");
1006    }
1007}
1008
1009
1010  
1011function getOptions(instanceId) {
1012    var options = $("#selected-options-"+instanceId).val();
1013    return options ? JSON.parse(options) : null;
1014}
1015
1016
1017
1018
1019
1020    
1021  function updateUI(instanceId, qty=null){
1022
1023    if(qty == null){
1024        qty = parseInt($('#qty-' + instanceId).val());
1025    }
1026
1027    $('.qty-' + instanceId).text(qty);
1028
1029    const $productTotalSpan = $('.total-product-uuid-' + instanceId);
1030
1031    const totals = JSON.parse($('#total-'+instanceId).val() || '{}');
1032    const unitPrice = totals?.unit_price || totals?.total || 0;
1033
1034    var total = qty * unitPrice;
1035
1036    var discount = parseFloat($productTotalSpan.data('product-discount'));
1037
1038    if (discount) {
1039        var discounted = total - discount;
1040        $productTotalSpan.html(`<s>${currency.format(total)}</s> <span class="font-semibold">${currency.format(discounted)}</span>`);
1041    } else {
1042        $productTotalSpan.text(currency.format(total));
1043    }
1044}
1045
1046function updateDataAndUI(element, groupUuid, optionUuid, optionName, price, instanceId) {
1047
1048    let quantity;
1049
1050    if ($(element).hasClass('option-checkbox')) {
1051        quantity = $(element).is(":checked") ? 1 : 0;
1052    } 
1053    else if ($(element).hasClass('option-stepper')) {
1054        quantity = parseInt($(element).val(), 10);
1055        if (isNaN(quantity)) return;
1056    } 
1057    else {
1058        return;
1059    }
1060
1061    let selectedOptions = selectedOptionsMap[instanceId] || {};
1062
1063    if (!selectedOptions[groupUuid]) {
1064        selectedOptions[groupUuid] = {
1065            uuid: groupUuid,
1066            name: null,
1067            extra: null,
1068            options: {},
1069            subtotal: 0
1070        };
1071    }
1072
1073    selectedOptions[groupUuid].options[optionUuid] = {
1074        name: optionName,
1075        price: price,
1076        quantity: quantity,
1077        subtotal: price * quantity
1078    };
1079
1080    selectedOptions[groupUuid].subtotal =
1081        Object.values(selectedOptions[groupUuid].options)
1082            .reduce((sum, option) => sum + option.subtotal, 0);
1083
1084    selectedOptionsMap[instanceId] = selectedOptions;
1085
1086    showSelectedOptions(instanceId);
1087    showTotals(instanceId);
1088    updateUI(instanceId);
1089}
1090
1091function showSelectedOptions(instanceId) {
1092    $(`#selected-options-${instanceId}`)
1093        .val(JSON.stringify(selectedOptionsMap[instanceId] || {}, null, 2));
1094}
1095
1096  function showTotals(instanceId) {
1097    const selectedOptions = selectedOptionsMap[instanceId] || {};
1098    let subTotalExtra = 0;
1099    let subTotalNonExtra = 0;
1100
1101    const $productTotalSpan = $('.total-product-uuid-' + instanceId);
1102    const basePrice = parseFloat($productTotalSpan.attr('data-product-total')) || 0;
1103    const discount = parseFloat($productTotalSpan.attr('data-product-discount')) || 0;
1104    const productTotal = basePrice - discount;
1105
1106    Object.values(selectedOptions).forEach(group => {
1107        const isExtra = String(group.extra) === "true";
1108        if (isExtra) {
1109            subTotalExtra += group.subtotal;
1110        } else {
1111            subTotalNonExtra += group.subtotal;
1112        }
1113    });
1114
1115    const qty = parseInt($('#qty-' + instanceId).val(), 10) || 1;
1116    const totalPerUnit = productTotal + subTotalExtra + subTotalNonExtra;
1117    const total = qty * totalPerUnit;
1118    const totalFormatted = currency.format(total);
1119
1120    // ✅ Atualiza apenas os valores visuais dentro do botão (sem perder estrutura Blade)
1121    $(`.qty-${instanceId}`).text(qty);
1122    $(`.total-product-uuid-${instanceId}`).html(totalFormatted);
1123
1124    // Recoloca atributos, se forem usados depois
1125    $(`.total-product-uuid-${instanceId}`)
1126        .attr('data-product-total', basePrice)
1127        .attr('data-product-discount', discount);
1128
1129    // Atualizar input escondido para o carrinho
1130    const totalsOutputObject = {
1131        unit_price: totalPerUnit,
1132        subtotal_extra: subTotalExtra,
1133        subtotal_non_extra: subTotalNonExtra,
1134        discount: discount,
1135        total: total // já é qty * totalPerUnit
1136      };
1137      
1138
1139    $(`#total-${instanceId}`).val(JSON.stringify(totalsOutputObject, null, 2));
1140
1141    console.log(`✅ Botão atualizado com: ${qty} x ${totalPerUnit} = ${totalFormatted}`);
1142}
1143    let selectedOptionsMap = {};
1144
1145    // Antigo
1146    // function showTotals(productId) {
1147    //     const selectedOptions = selectedOptionsMap[productId] || {};
1148    //     let subTotalExtra = 0;
1149    //     let subTotalNonExtra = 0;
1150    //     // Get Product Price
1151    //     const $productTotalSpan = $('.total-product-uuid-' + productId);
1152    //     const productTotal = parseFloat($productTotalSpan.data('product-total') ?? 0);
1153    //     const discount = parseFloat($productTotalSpan.data('product-discount') ?? 0);
1154    //     Object.values(selectedOptions).forEach(group => {
1155    //       //   if (group.extra) {
1156    //       //       subTotalExtra += group.subtotal;
1157    //       //   } else {
1158    //       //       subTotalNonExtra += group.subtotal;
1159    //       //   }
1160    //       const isExtra = String(group.extra) === "true";
1161    //       if (isExtra) {
1162    //           subTotalExtra += group.subtotal;
1163    //       } else {
1164    //           subTotalNonExtra += group.subtotal;
1165    //       }
1166  
1167    //     });
1168    //     const total = subTotalExtra + subTotalNonExtra + productTotal;
1169    //     const $addToCartBtn = $(`.add-to-cart[data-product-uuid="${productId}"]`);
1170    //     $addToCartBtn.attr('data-product-total', total);
1171  
1172    //     // Atualiza o botão visível com o novo valor total
1173    //       const totalFormatted = currency.format(total);
1174    //       $addToCartBtn.find(`.total-product-uuid-${productId}`).text(totalFormatted);
1175  
1176    //     // Create Total JSON
1177    //     const totalsOutputObject = {
1178    //       subtotal_extra: subTotalExtra,
1179    //       subtotal_non_extra: subTotalNonExtra,
1180    //       discount: discount,
1181    //       total: total
1182    //     };
1183    //     // Show
1184        
1185    //       const totalsOutputJSON = JSON.stringify(totalsOutputObject, null, 2);
1186    //     $(`#total-${productId}`).val(totalsOutputJSON);
1187    //    //$(`#total-${productId}`).val(JSON.stringify(totalsOutputObject, null, 2));
1188  
1189    //   }
1190
1191
1192
1193    
1194    function showNavbarSearch() {
1195        var navbarAreaSearch = $('#navbar-area-search');
1196        var logoAreaSearch = $('#logo-area-search');
1197        var closeAreaSearch = $('#close-area-search');
1198        
1199        navbarAreaSearch.removeClass('grid-cols-5 space-x-4').addClass('grid-cols-1');
1200        logoAreaSearch.addClass('hidden').removeClass("lg:inline-flex");
1201        closeAreaSearch.removeClass('hidden');
1202        
1203        $('#input-navbar-search').addClass('lg:col-span-5');
1204    }
1205    
1206    function hideNavbarSearch() {
1207        setTimeout(() => {
1208            var navbarAreaSearch = $('#navbar-area-search');
1209            var logoAreaSearch = $('#logo-area-search');
1210            var closeAreaSearch = $('#close-area-search');
1211
1212            navbarAreaSearch.removeClass('grid-cols-1').addClass('grid-cols-5  space-x-4');
1213            logoAreaSearch.removeClass('hidden').addClass("lg:inline-flex");
1214            closeAreaSearch.addClass('hidden');
1215            $('#input-navbar-search').removeClass('lg:col-span-5');
1216        }, 10);
1217    }
1218
1219    window.initStoreContext = async function () {
1220        const storeId = localStorage.getItem('store_id');
1221        const storeName = localStorage.getItem('store_name');
1222        const storeOptions = localStorage.getItem('store_options');
1223
1224        // Atualiza objeto global
1225        window.storeContext = {
1226            id: storeId,
1227            name: storeName,
1228            options: storeOptions ? JSON.parse(storeOptions) : {}
1229        };
1230
1231        // Já existe no Laravel session? (definido no Blade via window.storeId)
1232        if (typeof window.storeId === 'undefined' && storeId) {
1233            try {
1234                const response = await fetch('/set-store-session', {
1235                    method: 'POST',
1236                    headers: {
1237                        'Content-Type': 'application/json',
1238                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
1239                    },
1240                    body: JSON.stringify({ store_id: storeId })
1241                });
1242
1243                const result = await response.json();
1244                console.log('Store context sync:', result);
1245            } catch (e) {
1246                console.warn('Erro ao sincronizar store_id com sessão:', e);
1247            }
1248        }
1249    };
1250
1251
1252// ---------------------------------------------------------------------------
1253// sqvUpdateStoreState(data) — atualiza a UI da store sem reload completo
1254// ---------------------------------------------------------------------------
1255window.sqvFetchStoreState = async function (payload) {
1256    const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
1257    const requestPayload = { ...(payload || {}) };
1258
1259    if (typeof requestPayload.cart === 'undefined') {
1260        const localCart = localStorage.getItem('cart');
1261        if (localCart !== null) {
1262            requestPayload.cart = localCart;
1263        }
1264    }
1265
1266    try {
1267        const res = await fetch('/api/store/state', {
1268            method: 'POST',
1269            headers: {
1270                'Content-Type': 'application/json',
1271                'X-CSRF-TOKEN': csrfToken,
1272            },
1273            body: JSON.stringify(requestPayload),
1274        });
1275
1276        if (res.status === 419) { window.location.reload(); return null; }
1277        if (!res.ok) return null;
1278        const data = await res.json();
1279        sqvUpdateStoreState(data);
1280        return data;
1281    } catch (e) {
1282        console.error('[SQV] Erro ao atualizar estado da store:', e);
1283        return null;
1284    }
1285};
1286
1287window.sqvUpdateStoreState = function (data) {
1288    // 0.1 Remove do carrinho itens diários inválidos para o horário atual/agendado
1289    const invalidDailyIds = Array.isArray(data.invalidDailyCartProductIds)
1290        ? data.invalidDailyCartProductIds.map(function (id) { return String(id); })
1291        : [];
1292
1293    const normalizeProductId = function (rawId) {
1294        if (rawId === null || typeof rawId === 'undefined') {
1295            return '';
1296        }
1297
1298        const strId = String(rawId);
1299        const tailMatch = strId.match(/(\d+)$/);
1300        return tailMatch ? tailMatch[1] : strId;
1301    };
1302
1303    const invalidDailyIdSet = new Set(
1304        invalidDailyIds
1305            .map(function (id) { return normalizeProductId(id); })
1306            .filter(function (id) { return id !== ''; })
1307    );
1308
1309    if (invalidDailyIdSet.size > 0) {
1310        try {
1311            const currentCart = JSON.parse(localStorage.getItem('cart') || '[]');
1312            if (Array.isArray(currentCart) && currentCart.length > 0) {
1313                const itemsToRemove = currentCart.filter(function (item) {
1314                    return invalidDailyIdSet.has(normalizeProductId(item.id));
1315                });
1316
1317                if (itemsToRemove.length > 0) {
1318                    if (typeof removeFromCart === 'function') {
1319                        itemsToRemove.forEach(function (item) {
1320                            if (item && item.cart_id) {
1321                                removeFromCart(item.cart_id);
1322                            }
1323                        });
1324                    } else {
1325                        const filteredCart = currentCart.filter(function (item) {
1326                            return !invalidDailyIdSet.has(normalizeProductId(item.id));
1327                        });
1328                        localStorage.setItem('cart', JSON.stringify(filteredCart));
1329                        if (typeof updateCart === 'function') {
1330                            updateCart();
1331                        }
1332                    }
1333
1334                    if (typeof saveData === 'function') {
1335                        saveData('cart', localStorage.getItem('cart') || '[]');
1336                    }
1337
1338                    if (typeof toast === 'function') {
1339                        toast('warning', 'Produto diário removido do carrinho por incompatibilidade 
1339com o horário agendado.', 4500);
1340                    }
1341                }
1342            }
1343        } catch (e) {
1344            console.warn('[SQV] Falha ao reconciliar carrinho com produtos diários:', e);
1345        }
1346    }
1347
1348    // 0. Atualiza flag global de loja aberta/fechada (usado por abrirModalProdutoComVerificacao)
1349    window.sqvStoreIsOpen = data.isOpen;
1350
1351    // 1. Overlay fechado sobre a imagem da store
1352    const overlay = document.getElementById('sqv-store-closed-overlay');
1353    if (overlay) {
1354        overlay.classList.toggle('hidden', data.isOpen);
1355        const reasonEl = overlay.querySelector('.sqv-overlay-reason');
1356        if (reasonEl) reasonEl.textContent = data.closedReason;
1357    }
1358
1359    // 2. Bloco de alerta (razão de fecho)
1360    const alertEl = document.getElementById('sqv-store-alert');
1361    if (alertEl) {
1362        alertEl.classList.toggle('hidden', data.isOpen || !data.showChangeModeButtons);
1363        const reasonSpan = document.getElementById('sqv-closed-reason');
1364        if (reasonSpan) reasonSpan.textContent = data.closedReason;
1365    }
1366
1367    // 3. Container dos botões de mudança de modo
1368    const modeButtons = document.getElementById('sqv-mode-change-buttons');
1369    if (modeButtons) {
1370        modeButtons.classList.toggle('hidden', !data.showChangeModeButtons);
1371    }
1372
1373    // 4. Botão de checkout — desativa se a loja estiver fechada
1374    // Aplica apenas nos CTAs fixos de checkout para não afetar o conteúdo do popover do carrinho.
1375    // Mantemos a aparência do botão (sem opacidade) e controlamos apenas interação.
1376    document.querySelectorAll('.checkout-btn.fixed').forEach(function (checkoutBtn) {
1377        checkoutBtn.classList.toggle('pointer-events-none', !data.isOpen);
1378    });
1379
1380    // 5. Label de horário
1381    const label = data.scheduledLabel;
1382    document.querySelectorAll('.filter-time').forEach(el => { el.value = label; });
1383    document.querySelectorAll('.data-order-time').forEach(el => { el.textContent = label; });
1384
1385    // 6. Botões de modo — atualiza classe ativa
1386    document.querySelectorAll('.button-mode').forEach(btn => {
1387        btn.classList.toggle('button-active', btn.dataset.mode === data.orderMode);
1388    });
1389
1390    // 7. Toast informativo quando o estado muda
1391    if (!data.isOpen && data.showChangeModeButtons && data.closedReason && typeof toast === 'function') {
1392        toast('warning', data.closedReason, 4000);
1393    }
1394
1395    // 8. Secção de morada no checkout — mostra quando delivery, esconde quando pickup
1396    const deliverySection = document.getElementById('sqv-checkout-delivery-section');
1397    if (deliverySection) {
1398        deliverySection.classList.toggle('hidden', data.orderMode !== 'delivery');
1399    }
1400
1401    // 9. Sem morada em modo delivery → abre modal para preencher morada (na store page)
1402    if (data.needsAddress && typeof app !== 'undefined') {
1403        setTimeout(() => app.popup.open('.modal-address'), 350);
1404    }
1405
1406    // 10. Toggling das secções abertas/fechadas nos modais de produto
1407    document.querySelectorAll('.sqv-product-open-section').forEach(function (el) {
1408        el.classList.toggle('hidden', !data.isOpen);
1409    });
1410    document.querySelectorAll('.sqv-product-closed-section').forEach(function (el) {
1411        el.classList.toggle('hidden', data.isOpen);
1412    });
1413
1414    // Evento global para componentes que precisam reagir ao estado atualizado
1415    window.dispatchEvent(new CustomEvent('sqv:store-state-updated', { detail: data }));
1416};

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.