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, "&").replace(/</g, "<").replace(/>/g, ">") 783 .replace(/"/g, """).replace(/'/g, "'"); 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.