1/* ========================================================== 2 master.js â catálogo + filtros (AJAX completo) + UX + shopcart 3 Requer: jQuery 1.11.x+ 4 ========================================================== */ 5 6/* ========================================================== 7 * TABLE OF CONTENTS 8 * ---------------------------------------------------------- 9 * 0. Utilities & Language Helpers 10 * 1. Catalog State + Filter Rendering 11 * 2. Event Binding Dispatcher 12 * 2.1 Header & Navigation 13 * 2.2 Favorites 14 * 2.3 Currency Switcher 15 * 2.4 Catalog Controls & Filters 16 * 2.5 Menu Toggles & Scroll Helpers 17 * 2.6 Product Page Interactions 18 * 2.7 Cart Quantity, Discounts & Checkout 19 * 2.8 Gift Toggles, Delivery API Language & Live Search 20 * 2.9 Cart Item Removal 21 * 3. Mini-cart Hover + Cart Utilities 22 * 4. Products, Filters & Misc Utilities 23 * 5. Account / Authentication 24 * 6. Delivery / Checkout Helpers 25 * 7. Tools Popover, Search Helpers & Analytics 26 * 8. Bootstrapping 27 * ========================================================== */ 28 29(function ($) { 30 if (!$) return; 31 32 /* ========================================================== 33 * GLOBAL CONFIG 34 * ---------------------------------------------------------- 35 * Toggle `ajax.devMode` to choose between the development and 36 * production request handlers. Production must always resolve to 37 * `/request.php`. 38 * ========================================================== */ 39 var hasWindow = typeof window !== "undefined"; 40 var ajaxConfig = { 41 devMode: false, 42 prodEndpoint: 43 (hasWindow && window.CUPIDO_REQUEST_ENDPOINT) || "/request.php", 44 devEndpoint: "/request.php" 45 }; 46 var REQUEST_ENDPOINT = ajaxConfig.devMode 47 ? ajaxConfig.devEndpoint 48 : ajaxConfig.prodEndpoint; 49 50 function readQueryParam(name) { 51 if (!hasWindow || !name) return null; 52 var escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 53 var pattern = new RegExp("[?&]" + escaped + "=([^&#]*)"); 54 var match = pattern.exec(window.location.search || ""); 55 return match ? decodeURIComponent(match[1].replace(/\+/g, " ")) : null; 56 } 57 58 var filtersDebugFeature = (function () { 59 var raw = readQueryParam("filters_debug_feature"); 60 if (raw === null || raw === "") return null; 61 var num = parseInt(raw, 10); 62 return isNaN(num) ? null : num; 63 })(); 64 65 var filtersDebugToken = (function () { 66 var raw = readQueryParam("filters_debug"); 67 return raw === null || raw === "" ? null : raw; 68 })(); 69 70 if (hasWindow) { 71 window.CUPIDO_FILTERS_DEBUG_FEATURE = filtersDebugFeature; 72 } 73 74 var FILTER_QUERY_PARAM_MAP = { 75 color: "color_filters", 76 size: "sizes_filters", 77 composition: "composition_filters", 78 flavor: "flavor_filters", 79 brand: "brand_filters", 80 feature: "caracteristicas_filters" 81 }; 82 83 function getUrlSearchParams() { 84 if (!hasWindow || typeof window.URLSearchParams === "undefined") return null; 85 try { 86 return new URLSearchParams(window.location.search || ""); 87 } catch (e) { 88 return null; 89 } 90 } 91 92 function parseFilterListParam(raw) { 93 if (raw === null || raw === undefined || raw === "") return []; 94 return String(raw) 95 .split(",") 96 .map(function (item) { return parseInt($.trim(item), 10); }) 97 .filter(function (item, index, list) { 98 return !isNaN(item) && item > 0 && list.indexOf(item) === index; 99 }) 100 .map(function (item) { return String(item); }); 101 } 102 103 function parseFilterPriceParam(raw) { 104 if (raw === null || raw === undefined || raw === "") return null; 105 var parsed = parseFloat(String(raw).replace(",", ".")); 106 return isFinite(parsed) ? parsed : null; 107 } 108 109 function readFiltersStateFromUrl() { 110 var params = getUrlSearchParams(); 111 if (!params) return null; 112 113 var hasKnownParams = params.has("filters"); 114 var state = { 115 color_filters: [], 116 sizes_filters: [], 117 composition_filters: [], 118 flavor_filters: [], 119 brand_filters: [], 120 caracteristicas_filters: [], 121 price_filters: {} 122 }; 123 124 $.each(FILTER_QUERY_PARAM_MAP, function (queryKey, stateKey) { 125 if (!params.has(queryKey)) return; 126 hasKnownParams = true; 127 state[stateKey] = parseFilterListParam(params.get(queryKey)); 128 }); 129 130 if (params.has("price_min")) { 131 hasKnownParams = true; 132 var min = parseFilterPriceParam(params.get("price_min")); 133 if (min !== null) state.price_filters.min = min; 134 } 135 if (params.has("price_max")) { 136 hasKnownParams = true; 137 var max = parseFilterPriceParam(params.get("price_max")); 138 if (max !== null) state.price_filters.max = max; 139 } 140 141 return hasKnownParams ? state : null; 142 } 143 144 function syncFiltersStateToUrl(filters) { 145 var params = getUrlSearchParams(); 146 if (!params || !(history && history.replaceState)) return; 147 148 $.each(FILTER_QUERY_PARAM_MAP, function (queryKey) { 149 params.delete(queryKey); 150 }); 151 params.delete("filters"); 152 params.delete("price_min"); 153 params.delete("price_max"); 154 155 if (hasSessionFilters(filters)) { 156 params.set("filters", "1"); 157 158 $.each(FILTER_QUERY_PARAM_MAP, function (queryKey, stateKey) { 159 var list = []; 160 var raw = filters && filters[stateKey]; 161 if (Array.isArray(raw)) { 162 list = raw; 163 } else if (raw && typeof raw === "object") { 164 list = Object.keys(raw).map(function (key) { return raw[key]; }); 165 } 166 list = parseFilterListParam((list || []).join(",")); 167 if (list.length) { 168 params.set(queryKey, list.join(",")); 169 } 170 }); 171 172 var priceFilters = filters && filters.price_filters ? filters.price_filters : null; 173 var min = priceFilters ? parseFilterPriceParam(priceFilters.min) : null; 174 var max = priceFilters ? parseFilterPriceParam(priceFilters.max) : null; 175 if (min !== null) params.set("price_min", String(min)); 176 if (max !== null) params.set("price_max", String(max)); 177 } 178 179 var nextSearch = params.toString(); 180 var nextUrl = window.location.pathname + (nextSearch ? "?" + nextSearch : "") + window.location.hash; 181 history.replaceState(history.state || {}, "", nextUrl); 182 } 183 184 /* ========================================================== 185 * 0) HELPERS GERAIS 186 * ========================================================== */ 187 function postHTML(action, data, ok) { 188 return $.ajax({ 189 type: "POST", url: REQUEST_ENDPOINT, dataType: "html", cache: false, 190 data: $.extend({ ajax: action }, data || {}), success: ok 191 }); 192 } 193 function postJSON(action, data, ok) { 194 // Algumas ações não devolvem JSON válido 195 var dt = (action === "updateFilters" || action === "restoreFilters") ? "text" : "json"; 196 return $.ajax({ 197 type: "POST", url: REQUEST_ENDPOINT, dataType: dt, cache: false, 198 data: $.extend({ ajax: action }, data || {}), success: ok 199 }); 200 } 201 var FILTER_PERSIST_DEBUG = true; 202 var fpLog = function () { 203 if (!FILTER_PERSIST_DEBUG || !console || !console.log) return; 204 var args = Array.prototype.slice.call(arguments); 205 args.unshift("[filters-persist]"); 206 console.log.apply(console, args); 207 }; 208 var gv = s => { var $e = $(s); return $e.length ? ($e.val() || "") : ""; }; 209 var sv = (s, v) => { var $e = $(s); if ($e.length) $e.val(v); }; 210 211 function currentLangCode(){ 212 var fromInput = (document.getElementById('session_lang') || {}).value; 213 var fromUrl = (location.pathname.match(/^\/(pt|en|es)(?:\/|$)/i) || [,''])[1]; 214 var fromHtml = (document.documentElement.getAttribute('lang') || '').toLowerCase(); 215 var code = (fromInput || fromUrl || fromHtml || 'pt').toLowerCase(); 216 if (!/^(pt|en|es)$/.test(code)) code = 'pt'; 217 return code; 218 } 219 220 function gl(key){ 221 if (!key) return ''; 222 var pageLang = currentLangCode(); 223 var dict; 224 try { dict = JSON.parse(localStorage.getItem('langObj') || 'null'); } catch(e){ dict = null; } 225 226 if (dict && dict[key] != null && (!dict.code || String(dict.code).toLowerCase() === pageLang)) { 227 return String(dict[key]); 228 } 229 230 var FB = { 231 pt: { 232 ITEM: ' item', 233 ITEMS: ' itens', 234 swal_attention: 'Atenção', 235 swal_missing_fields: 'Por favor verifique se preencheu todos os campos!', 236 swal_login_failed: 'Login falhou', 237 swal_btnok: 'OK', 238 swal_btncancel: 'Cancelar', 239 swal_btnyes: 'Sim', 240 QTD: 'un.', 241 SHOPCART_REMOVE_MODAL_UNIT: 'un.', 242 SHOPCART_DRAWER_EYEBROW: 'Cesto de compras', 243 SHOPCART_DRAWER_TITLE: 'Cesto', 244 SHOPCART_DRAWER_CLOSE: 'Fechar', 245 SHOPCART_DRAWER_SUBTOTAL: 'Subtotal', 246 SHOPCART_DRAWER_SHIPPING: 'Portes', 247 SHOPCART_DRAWER_SHIPPING_PENDING: 'A calcular', 248 SHOPCART_DRAWER_SAVINGS: 'Descontos', 249 SHOPCART_DRAWER_TOTAL: 'Total', 250 SHOPCART_DRAWER_CHECKOUT: 'FINALIZAR COMPRA', 251 SHOPCART_DRAWER_CART: 'IR PARA O CESTO', 252 SHOPCART_REMOVE_MODAL_TITLE: 'Remover do cesto?', 253 SHOPCART_SAVE_FOR_LATER_MESSAGE: 'Guarde-o nos favoritos para o encontrar mais tarde.', 254 SHOPCART_SAVE_FOR_LATER: 'Guardar para depois', 255 SHOPCART_SAVED_FOR_LATER_ADD_ALL: 'Adicionar tudo ao cesto', 256 SHOPCART_SAVED_FOR_LATER_LIST: 'Guardados para mais tarde', 257 SHOPCART_REMOVE: 'Remover', 258 SHOPCART_ADDED_OK: 'Adicionado ao carrinho', 259 CESTO_VAZIO1: 'O cesto está vazio',
260 CESTO_VAZIO2: 'Vê as novidades da loja.', 261 CESTO_VAZIO3: 'Ver produtos', 262 CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TITLE: 'Listas e Favoritos', 263 CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TEXT: 'Para usar a funcionalidade de Listas e Favoritos, por favor faça login ou registe-se', 264 CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_REGISTER: 'Registar', 265 CV2_STOCK_LAST_UNIT: 'Ãltima unidade disponÃvel', 266 CV2_STOCK_LAST_HINT: 'Outro cliente pode concluir a compra primeiro', 267 CV2_STOCK_LOW: 'Só restam {N} unidades', 268 CV2_STOCK_SOLDOUT_BADGE: 'Esgotado', 269 CV2_STOCK_SOLDOUT_MSG: 'Este artigo ficou sem stock', 270 CV2_STOCK_UPDATED: 'Stock atualizado', 271 CV2_STOCK_ADJUST: 'Apenas {N} unidades disponÃveis', 272 CV2_STOCK_ADJUST_BTN: 'Ajustar para {N}', 273 CV2_STOCK_NOTIFY: 'Avisar-me quando disponÃvel', 274 CV2_STOCK_REMOVE: 'Remover do cesto', 275 CV2_STOCK_BLOCK_MSG: 'Um artigo do seu cesto ficou indisponÃvel', 276 CV2_STOCK_BLOCK_CTA: 'Remover indisponÃveis e continuar', 277 CV2_SHIPPING_ADDRESS_REQUIRED: 'Indique a morada de entrega completa (nome, morada, código postal, localidade e paÃs) antes de pagar.', 278 CV2_SHIPPING_PHONE_REQUIRED: 'Indique um telemóvel válido (9 a 15 dÃgitos) antes de pagar.' 279 }, 280 en: { 281 ITEM: ' item', 282 ITEMS: ' items', 283 swal_attention: 'Attention', 284 swal_missing_fields: 'Please verify if you have completed all the required fields!', 285 swal_login_failed: 'Login failed', 286 swal_btnok: 'OK', 287 swal_btncancel: 'Cancel', 288 swal_btnyes: 'Yes', 289 QTD: 'un.', 290 SHOPCART_REMOVE_MODAL_UNIT: 'un.', 291 SHOPCART_DRAWER_EYEBROW: 'Shopping bag', 292 SHOPCART_DRAWER_TITLE: 'Bag', 293 SHOPCART_DRAWER_CLOSE: 'Close', 294 SHOPCART_DRAWER_SUBTOTAL: 'Subtotal', 295 SHOPCART_DRAWER_SHIPPING: 'Shipping', 296 SHOPCART_DRAWER_SHIPPING_PENDING: 'Calculating', 297 SHOPCART_DRAWER_SAVINGS: 'Discounts', 298 SHOPCART_DRAWER_TOTAL: 'Total', 299 SHOPCART_DRAWER_CHECKOUT: 'CHECKOUT', 300 SHOPCART_DRAWER_CART: 'GO TO BAG', 301 SHOPCART_REMOVE_MODAL_TITLE: 'Remove from bag?', 302 SHOPCART_SAVE_FOR_LATER_MESSAGE: 'Save it to find it later.', 303 SHOPCART_SAVE_FOR_LATER: 'Save for later', 304 SHOPCART_SAVED_FOR_LATER_ADD_ALL: 'Add all to bag', 305 SHOPCART_SAVED_FOR_LATER_LIST: 'Saved for later', 306 SHOPCART_REMOVE: 'Remove', 307 SHOPCART_ADDED_OK: 'Added to bag', 308 CESTO_VAZIO1: 'Your bag is empty', 309 CESTO_VAZIO2: "See what's new in the shop.", 310 CESTO_VAZIO3: 'See products', 311 CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TITLE: 'Lists and Favourites', 312 CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TEXT: 'To use Lists and Favourites, please sign in or register', 313 CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_REGISTER: 'Register', 314 CV2_STOCK_LAST_UNIT: 'Last unit available', 315 CV2_STOCK_LAST_HINT: 'Another customer may complete the purchase first', 316 CV2_STOCK_LOW: 'Only {N} units left', 317 CV2_STOCK_SOLDOUT_BADGE: 'Sold out', 318 CV2_STOCK_SOLDOUT_MSG: 'This item is out of stock', 319 CV2_STOCK_UPDATED: 'Stock updated', 320 CV2_STOCK_ADJUST: 'Only {N} units available', 321 CV2_STOCK_ADJUST_BTN: 'Adjust to {N}', 322 CV2_STOCK_NOTIFY: 'Notify me when available', 323 CV2_STOCK_REMOVE: 'Remove from cart', 324 CV2_STOCK_BLOCK_MSG: 'An item in your cart became unavailable', 325 CV2_STOCK_BLOCK_CTA: 'Remove unavailable and continue', 326 CV2_SHIPPING_ADDRESS_REQUIRED: 'Please enter a complete delivery address (name, address, postal code, city and country) before paying.', 327 CV2_SHIPPING_PHONE_REQUIRED: 'Please enter a valid mobile number (9 to 15 digits) before paying.' 328 }, 329 es: { 330 ITEM: ' artÃculo', 331 ITEMS: ' artÃculos', 332 swal_attention: 'Atención', 333 swal_missing_fields: 'Por favor, compruebe que ha llenado todos los campos!', 334 swal_login_failed: 'Login fallido', 335 swal_btnok: 'OK', 336 swal_btncancel: 'Cancelar', 337 swal_btnyes: 'SÃ', 338 QTD: 'ud.', 339 SHOPCART_REMOVE_MODAL_UNIT: 'ud.', 340 SHOPCART_DRAWER_EYEBROW: 'Cesta de compras', 341 SHOPCART_DRAWER_TITLE: 'Cesta', 342 SHOPCART_DRAWER_CLOSE: 'Cerrar', 343 SHOPCART_DRAWER_SUBTOTAL: 'Subtotal', 344 SHOPCART_DRAWER_SHIPPING: 'EnvÃo', 345 SHOPCART_DRAWER_SHIPPING_PENDING: 'Calculando', 346 SHOPCART_DRAWER_SAVINGS: 'Descuentos', 347 SHOPCART_DRAWER_TOTAL: 'Total', 348 SHOPCART_DRAWER_CHECKOUT: 'FINALIZAR COMPRA', 349 SHOPCART_DRAWER_CART: 'IR A LA CESTA', 350 SHOPCART_REMOVE_MODAL_TITLE: '¿Quitar de la cesta?', 351 SHOPCART_SAVE_FOR_LATER_MESSAGE: 'Guárdalo para encontrarlo más tarde.', 352 SHOPCART_SAVE_FOR_LATER: 'Guardar para más tarde', 353 SHOPCART_SAVED_FOR_LATER_ADD_ALL: 'Añadir todo a la cesta', 354 SHOPCART_SAVED_FOR_LATER_LIST: 'Guardados para más tarde', 355 SHOPCART_REMOVE: 'Eliminar', 356 SHOPCART_ADDED_OK: 'Añadido a la cesta', 357 CESTO_VAZIO1: 'La cesta está vacÃa', 358 CESTO_VAZIO2: 'Mira las novedades de la tienda.', 359 CESTO_VAZIO3: 'Ver productos',
360 CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TITLE: 'Listas y Favoritos', 361 CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TEXT: 'Para usar Listas y Favoritos, inicie sesión o regÃstrese', 362 CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_REGISTER: 'Registrarse', 363 CV2_STOCK_LAST_UNIT: 'Ãltima unidad disponible', 364 CV2_STOCK_LAST_HINT: 'Otro cliente puede completar la compra primero', 365 CV2_STOCK_LOW: 'Solo quedan {N} unidades', 366 CV2_STOCK_SOLDOUT_BADGE: 'Agotado', 367 CV2_STOCK_SOLDOUT_MSG: 'Este artÃculo se quedó sin stock', 368 CV2_STOCK_UPDATED: 'Stock actualizado', 369 CV2_STOCK_ADJUST: 'Solo {N} unidades disponibles', 370 CV2_STOCK_ADJUST_BTN: 'Ajustar a {N}', 371 CV2_STOCK_NOTIFY: 'Avisarme cuando esté disponible', 372 CV2_STOCK_REMOVE: 'Quitar del carrito', 373 CV2_STOCK_BLOCK_MSG: 'Un artÃculo de su cesta quedó no disponible', 374 CV2_STOCK_BLOCK_CTA: 'Quitar no disponibles y continuar', 375 CV2_SHIPPING_ADDRESS_REQUIRED: 'Indique la dirección de entrega completa (nombre, dirección, código postal, localidad y paÃs) antes de pagar.', 376 CV2_SHIPPING_PHONE_REQUIRED: 'Indique un móvil válido (9 a 15 dÃgitos) antes de pagar.' 377 } 378 }; 379 var pack = FB[pageLang] || FB.pt; 380 if (pack[key] != null) return pack[key]; 381 if (FB.pt[key] != null) return FB.pt[key]; 382 return key; 383 } 384 window.gl = gl; 385 386 // codigo de desconto partilhado entre sidebar e checkout (persistido no browser; 387 // sem isto, o populate de uma pagina com input vazio apagava o desconto da outra) 388 function cv2GetDiscount(){ try { return localStorage.getItem("cv2_discount_code") || ""; } catch (e) { return ""; } } 389 function cv2SetDiscount(c){ try { if (c) localStorage.setItem("cv2_discount_code", c); else localStorage.removeItem("cv2_discount_code"); } catch (e) {} } 390 window.cv2GetDiscount = cv2GetDiscount; 391 window.cv2SetDiscount = cv2SetDiscount; 392 393 // pais de entrega escolhido (guest) partilhado entre sidebar e checkout 394 function cv2GetCountry(){ try { return localStorage.getItem("cv2_country_id") || ""; } catch (e) { return ""; } } 395 function cv2SetCountry(c){ try { if (c) localStorage.setItem("cv2_country_id", String(c)); else localStorage.removeItem("cv2_country_id"); } catch (e) {} } 396 window.cv2GetCountry = cv2GetCountry; 397 window.cv2SetCountry = cv2SetCountry; 398 399 // Mantém apenas meta aqui â NÃO tocar em 'langObj' 400 function ensureLangMeta() { 401 var fromInput = (document.getElementById('session_lang') || {}).value; 402 var fromUrl = (location.pathname.match(/^\/(pt|en|es)(?:\/|$)/i) || [,''])[1]; 403 var fromHtml = (document.documentElement.getAttribute('lang') || '').toLowerCase(); 404 var code = (fromInput || fromUrl || fromHtml || 'pt').toLowerCase(); 405 if (!/^(pt|en|es)$/.test(code)) code = 'pt'; 406 407 var meta = { code, clang_id: code, locale: code==='pt'?'pt-PT':code==='es'?'es-ES':'en-GB', currency: 'EUR', symbol: 'â¬', ts: Date.now() }; 408 try { localStorage.setItem('langMeta', JSON.stringify(meta)); } catch(e){} 409 if (window.Cookies) Cookies.set('clang_id', code, { expires: 365, path: '/' }); 410 return meta; 411 } 412 413 414 function fetchLangDict() { 415 var meta; 416 try { meta = JSON.parse(localStorage.getItem('langMeta') || 'null'); } catch(e){} 417 var lang = (meta && meta.code) || 'pt'; 418 419 return $.ajax({ 420 type: "POST", 421 url: REQUEST_ENDPOINT, 422 data: { ajax: "getLanguageObj", lang_id: lang }, 423 dataType: "json", // <- importante! 424 cache: false 425 }).done(function(dict){ 426 try { 427 var merged = {}; 428 if (meta && typeof meta === 'object') merged = $.extend({}, meta); 429 if (dict && typeof dict === 'object') merged = $.extend(merged, dict); 430 if (!merged.code) merged.code = lang; 431 localStorage.setItem('langObj', JSON.stringify(merged)); 432 } catch(e){} 433 }); 434 } 435 436 /* ========================================================== 437 * 1) COOKIES + LÃNGUA 438 * ========================================================== */ 439 // Fallback caso js-cookie não esteja carregado 440 (function (w) { 441 if (!w.Cookies) { 442 w.Cookies = { 443 get: function (name) { 444 var m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1') + '=([^;]*)')); 445 return m ? decodeURIComponent(m[1]) : undefined; 446 }, 447 set: function (name, value, opts) { 448 opts = opts || {}; 449 var str = name + '=' + encodeURIComponent(value); 450 if (opts.expires) { 451 var d = new Date(); 452 d.setTime(d.getTime() + (opts.expires * 864e5)); // dias 453 str += '; expires=' + d.toUTCString(); 454 } 455 str += '; path=' + (opts.path || '/'); 456 document.cookie = str; 457 } 458 }; 459 } 460 })(window); 461 462 // Sincroniza cookie clang_id + localStorage langObj 463 function resetLangObj() { 464 var fromInput = (document.getElementById('session_lang') || {}).value; 465 var fromUrl = (location.pathname.match(/^\/(pt|en|es)(?:\/|$)/i) || [,''])[1]; 466 var fromHtml = (document.documentElement.getAttribute('lang') || '').toLowerCase(); 467 var code = (fromInput || fromUrl || fromHtml || 'pt').toLowerCase(); 468 if (!/^(pt|en|es)$/.test(code)) code = 'pt'; 469 470 var obj = { 471 code: code, 472 clang_id: code, // compat c
472om legado 473 locale: code === 'pt' ? 'pt-PT' : code === 'es' ? 'es-ES' : 'en-GB', 474 currency: 'EUR', 475 symbol: 'â¬', 476 ts: Date.now() 477 }; 478 479 try { 480 localStorage.setItem('langMeta', JSON.stringify(obj)); 481 var existingLangObj = JSON.parse(localStorage.getItem('langObj') || 'null'); 482 if (!existingLangObj || typeof existingLangObj !== 'object') existingLangObj = {}; 483 existingLangObj.code = obj.code; 484 existingLangObj.clang_id = obj.clang_id; 485 existingLangObj.locale = obj.locale; 486 existingLangObj.currency = obj.currency; 487 existingLangObj.symbol = obj.symbol; 488 existingLangObj.ts = obj.ts; 489 localStorage.setItem('langObj', JSON.stringify(existingLangObj)); 490 } catch (e) {} 491 Cookies.set('clang_id', code, { expires: 365, path: '/' }); 492 return obj; 493 } 494 495 // Garante que existe langObj + clang_id 496 (function ensureLang() { 497 var clang_id = Cookies.get('clang_id'); 498 var langObj; 499 try { langObj = JSON.parse(localStorage.getItem('langObj') || 'null'); } catch(e){ langObj = null; } 500 if (!clang_id || !langObj || !langObj.code) resetLangObj(); 501 })(); 502 503 /* ========================================================== 504 * 2) ESTADO DO CATÃLOGO 505 * ========================================================== */ 506 var Cat = { order: "", perPage: "32", page: "1" }; 507 var lastCatalogResultCount = 0; 508 var FILTER_CATEGORY_COOKIE = "catalog_filters_category"; 509 var FILTER_CATEGORY_STORAGE = "catalog_filters_last_cat"; 510 var FILTERS_STORAGE_PREFIX = "catalog_filters_"; 511 var FILTER_CATEGORY_FALLBACK = "catalog_filters_last_cat_cookie"; 512 var shouldResetFiltersOnBoot = false; 513 514 function getSavedFilterCategory() { 515 var stored = null; 516 try { stored = localStorage.getItem(FILTER_CATEGORY_STORAGE); } catch (e) { stored = null; } 517 if (!stored) stored = Cookies.get(FILTER_CATEGORY_COOKIE) || Cookies.get(FILTER_CATEGORY_FALLBACK) || null; 518 return stored; 519 } 520 521 function rememberFilterCategory(catId) { 522 if (!catId) return; 523 Cookies.set(FILTER_CATEGORY_COOKIE, String(catId), { expires: 30, path: "/" }); 524 Cookies.set(FILTER_CATEGORY_FALLBACK, String(catId), { expires: 30, path: "/" }); 525 try { localStorage.setItem(FILTER_CATEGORY_STORAGE, String(catId)); } catch (e) {} 526 } 527 528 function getFilterCategoryKey() { 529 return gv("#extra_query_filters") || gv("#extra_query") || ""; 530 } 531 532 function saveFiltersSnapshot(catId, filters) { 533 if (!catId) return; 534 var safe = filters || {}; 535 var normalizeList = function (v) { 536 if (Array.isArray(v)) return v; 537 if (v && typeof v === "object") return Object.keys(v).map(function (k) { return v[k]; }); 538 return []; 539 }; 540 safe = { 541 color_filters: normalizeList(safe.color_filters), 542 sizes_filters: normalizeList(safe.sizes_filters), 543 composition_filters: normalizeList(safe.composition_filters), 544 flavor_filters: normalizeList(safe.flavor_filters), 545 brand_filters: normalizeList(safe.brand_filters), 546 caracteristicas_filters: normalizeList(safe.caracteristicas_filters), 547 price_filters: safe.price_filters || {} 548 }; 549 try { 550 localStorage.setItem(FILTERS_STORAGE_PREFIX + catId, JSON.stringify(safe)); 551 fpLog("saved snapshot", { cat: catId, safe: safe }); 552 } catch (e) {} 553 } 554 555 function loadFiltersSnapshot(catId) { 556 if (!catId) return null; 557 try { 558 var raw = localStorage.getItem(FILTERS_STORAGE_PREFIX + catId); 559 var snap = raw ? JSON.parse(raw) : null; 560 fpLog("loaded snapshot", { cat: catId, snapshot: snap }); 561 return snap; 562 } catch (e) { 563 fpLog("error loading snapshot", e); 564 return null; 565 } 566 } 567 568 function buildFilterTasksFromSnapshot(catId, snapshot) { 569 var tasks = []; 570 var basePayload = payload(); 571 var toList = function (v) { 572 if (Array.isArray(v)) return v; 573 if (v && typeof v === "object") return Object.keys(v).map(function (k) { return v[k]; }); 574 return []; 575 }; 576 577 // reset first 578 tasks.push(function () { 579 fpLog("task: reset filters", { cat: catId }); 580 return postJSON("updateFilters", { value: valueMap(basePayload, { reset_all: 1 }) }); 581 }); 582 583 function pushToggleTasks(key, list) { 584 if (!Array.isArray(list)) return; 585 list.forEach(function (val) { 586 if (!val) return; 587 tasks.push(function () { 588 var extras = { nowpage: 1 }; 589 extras[key] = val; 590 return postJSON("updateFilters", { value: valueMap(basePayload, extras) }); 591 }); 592 }); 593 } 594 595 pushToggleTasks("color", toList(snapshot.color_filters)); 596 pushToggleTasks("size", toList(snapshot.sizes_filters)); 597 pushToggleTasks("composition", toList(snapshot.composition_filters)); 598 pushToggleTasks("flavor", toList(snapshot.flavor_filters)); 599 pushToggleTasks("brand", toList(snapshot.brand_filters)); 600 pushToggleTasks("feature", toList(snapshot.caracteristicas_filters)); 601 602 if (snapshot.price_filters && (snapshot.price_filters.min || snapshot.price_filters.max)) { 603 tasks.push(function () { 604 var min = snapshot.price_filters.min; 605 var max = snapshot.price_filters.max; 606 var payloadVals = {}; 607 if (min !== undefined && min !== null && min !== "") payloadVals.price_min = parseFloat(min); 608 if (max !== undefined && max !== null && max !== "") payloadVals.price_max = parseFloat(max); 609 var payload = { value: valueMap(basePayload, payloadVals) }; 610 fpLog("task: price", payload); 611 return postJSON("updateFilters", payload); 612 }); 613 } 614 615 fpLog("built tasks", { cat: catId, total: tasks.length }); 616 return tasks; 617 } 618 619 function runTasksSequentially(tasks, done) { 620 var idx = 0; 621 var next = function () { 622 if (!tasks.length) { 623 fpLog("tasks finished"); 624 if (typeof done === "function") done(); 625 return; 626 } 627 var task = tasks.shift();
628 fpLog("task start", { index: idx }); 629 idx += 1; 630 var req = task(); 631 if (req && typeof req.always === "function") { 632 req.always(next); 633 } else { 634 next(); 635 } 636 }; 637 next(); 638 } 639 640 function initState() { 641 var urlPage = (location.pathname.match(/\/page\/(\d+)\//) || [null, null])[1]; 642 Cat.order = gv("#order") || $(".webstore-order:first").val() || ""; 643 Cat.perPage = gv("#espaco_lista") || $(".webstore-results-num:first").val() || "32"; 644 Cat.page = gv("#nowpage") || urlPage || $(".webstore-direct-page:first").val() || $(".webstore-page-num:first").val() || "1"; 645 sv("#order", Cat.order); sv("#espaco_lista", Cat.perPage); sv("#nowpage", Cat.page); 646 } 647 function payload() { 648 var base = { 649 espaco_lista: Cat.perPage, nowpage: Cat.page, 650 extra_query: gv("#extra_query"), 651 extra_query_marca: gv("#extra_query_marca") || (location.pathname.match(/\/brand\/(\d+)\//) || [null, ""])[1], 652 extra_query_tipo: gv("#extra_query_tipo"),
653 novidades: gv("#novidades"), 654 search: gv("#searchq") 655 }; 656 if (Cat.order) base.order = Cat.order; 657 if (filtersDebugFeature !== null) { 658 base.filters_debug_feature = filtersDebugFeature; 659 } 660 if (filtersDebugToken !== null) { 661 base.filters_debug = filtersDebugToken; 662 } 663 return base; 664 } 665 function valueMap(p, extra) { 666 var v = { nowpage: p.nowpage, espaco_lista: p.espaco_lista }; 667 if (p.order) v.order = p.order; 668 if (p.extra_query) v.extra_query = p.extra_query; 669 if (p.extra_query_marca) v.extra_query_marca = p.extra_query_marca; 670 if (p.extra_query_tipo) v.extra_query_tipo = p.extra_query_tipo; 671 if (p.novidades) v.novidades = p.novidades; 672 if (p.search) v.search = p.search; 673 if (p.filters_debug_feature !== null && p.filters_debug_feature !== undefined) { 674 v.filters_debug_feature = p.filters_debug_feature; 675 } 676 if (p.filters_debug !== null && p.filters_debug !== undefined) { 677 v.filters_debug = p.filters_debug; 678 } 679 return $.extend(v, extra || {}); 680 } 681 682 /* ========================================================== 683 * 3) URL /page/N/ 684 * ========================================================== */ 685 function pushPage(n) { 686 if (!(history && history.pushState)) return; 687 var path = location.pathname; 688 path = path.match(/\/page\/\d+\//) 689 ? path.replace(/\/page\/\d+\//, "/page/" + n + "/") 690 : path.replace(/\/?$/, "/") + "page/" + n + "/"; 691 history.pushState({ page: n }, "", path + location.search + location.hash); 692 } 693 694 /* ========================================================== 695 * 4) RENDERIZAÃÃO (filtros + catálogo) 696 * ========================================================== */ 697 function openSelected($root) { 698 if (!$root || !$root.length) return; 699 $root.find(".filter-table").each(function () { 700 var $table = $(this); 701 var hasActive = $table.find(".fa-selected, .is-selected, [aria-pressed='true'], :checked").length > 0; 702 var pref = readFilterAccordionPreference($table); 703 if (pref === true) { 704 setFilterAccordionState($table, true); 705 } else if (pref === false) { 706 setFilterAccordionState($table, false); 707 } else if (hasActive) { 708 setFilterAccordionState($table, true); 709 } 710 }); 711 } 712 713 function buildMobileFiltersMarkup(content) { 714 var clearLabel = gl("FILTER_CLEAR") || "Limpar filtros"; 715 var applyLabel = gl("FILTER_APPLY") || "Aplicar"; 716 var resultsLabel = gl("FILTER_RESULTS") || "resultados"; 717 var closeLabel = gl("swal_btncancel") || "Fechar"; 718 719 return ( 720 "<div class='mobile-filters-header'>" + 721 "<button type='button' class='mobile-filters-reset' disabled>" + clearLabel + "</button>" + 722 "<button type='button' class='mobile-filters-close' aria-label='" + closeLabel + "'>" + 723 "<i class='fa fa-times-circle fa-2x'></i>" + 724 "</button>" + 725 "</div>" + 726 "<div class='mobile-filters-content'>" + content + "</div>" + 727 "<div class='mobile-filters-footer'>" + 728 "<button type='button' class='mobile-filters-apply'>" + 729 applyLabel + " (<span class='mobile-filters-result-count'>0</span> " + resultsLabel + ")" + 730 "</button>" + 731 "</div>" 732 ); 733 } 734 735 function getFiltersPanelTitle() { 736 var meta; 737 var lang = "pt"; 738 try { meta = JSON.parse(localStorage.getItem("langMeta") || "null"); } catch (e) { meta = null; } 739 if (meta && meta.code) { 740 lang = meta.code.toLowerCase(); 741 } else if (window.Cookies && typeof Cookies.get === "function") { 742 lang = (Cookies.get("clang_id") || "pt").toLowerCase(); 743 } 744 var dictTitle = gl("FILTERS_TITLE"); 745 if (dictTitle && typeof dictTitle === "string") { 746 return dictTitle; 747 } 748 switch (lang) { 749 case "en": return "Filters"; 750 case "es": return "Filtros"; 751 default: return "Filtros"; 752 } 753 } 754 755 function buildDesktopFiltersMarkup(content) { 756 var clearLabel = gl("FILTER_CLEAR") || "Limpar filtros"; 757 var title = getFiltersPanelTitle(); 758 var promoHtml = ""; 759 var filterHtml = content; 760 761 if (content && content.indexOf("catalog-promo-filter-card") !== -1) { 762 var $tmp = $("<div>").html(content); 763 var $promo = $tmp.find(".catalog-promo-filter-card").first(); 764 if ($promo.length) { 765 promoHtml = $("<div>").append($promo.detach()).html(); 766 filterHtml = $tmp.html(); 767 } 768 } 769 770 return ( 771 "<div class='filters-panel' data-filters-shell='desktop'>" + 772 "<div class='filters-panel-header'>" + 773 "<span class='filters-panel-title'><i class='fa fa-filter' aria-hidden='true'></i> " + title + "</span>" + 774 "<button type='button' class='filters-panel-reset' aria-label='" + clearLabel + "'>" + 775 "<span>" + clearLabel + "</span>" + 776 "</button>" + 777 "</div>" + 778 "<div class='filters-panel-content'>" + filterHtml + "</div>" + 779 "</div>" + 780 promoHtml 781 ); 782 } 783 784 function updateDesktopFiltersReset(hasFilters) { 785 var $btn = $(".filters-panel-reset"); 786 if (!$btn.length) return; 787 var active = !!hasFilters; 788 $btn.toggleClass("is-active", active).prop("disabled", !active); 789 } 790 791 function updateMobileFiltersReset(hasFilters) { 792 var $btn = $(".mobile-filters-reset"); 793 if (!$btn.length) return; 794 var active = !!hasFilters; 795 $btn.toggleClass("is-active", active).prop("disabled", !active); 796 } 797 798 function prepareMobileFiltersState($root) { 799 if (!$root || !$root.length) return; 800 $root.find(".filter-table").each(function () { 801 var $table = $(this); 802 var hasActive = $table.find(".fa-selected, .is-selected, [aria-pressed='true'], :checked").length > 0; 803 var $header = findFilterHeader($table); 804 var target = $header.length ? $header : $table; 805 var pref = readFilterAccordionPreference($table); 806 var shouldOpen = (pref === null) ? hasActive : pref; 807 setFilterAccordionState(target, shouldOpen); 808 }); 809 syncSizeFilterGrid($root); 810 syncColorFilterGrid($root); 811 } 812 813 function syncSizeFilterGrid($root) { 814 if (!$root || !$root.length) return; 815 $root.find("#size-filter-table").each(function () { 816 $(this) 817 .find("a.filtersize") 818 .each(function () { 819 var $link = $(this); 820 var isSelected = $link.find(".fa").hasClass("fa-selected"); 821 $link.toggleClass("size-grid-selected", isSelected); 822 }); 823 }); 824 } 825 826 function syncColorFilterGrid($root) { 827 if (!$root || !$root.length) return; 828 $root.find("#color-filter-table").each(function () { 829 $(this) 830 .find("a.filtercolor") 831 .each(function () { 832 var $link = $(this); 833 var isSelected = $link.find(".fa").hasClass("fa-selected"); 834 $link.toggleClass("color-grid-selected", isSelected); 835 }); 836 }); 837 } 838 839 function initPriceFilterTables($root) { 840 if (!$root || !$root.length) return; 841 $root.find(".price-filter-table").each(function () { 842 var $table = $(this); 843 var bounds = getPriceFilterBounds($table); 844 var minVal = parsePriceValue($table.find(".price-filter-input-min").val()); 845 var maxVal = parsePriceValue($table.find(".price-filter-input-max").val()); 846 var range = normalizePriceRange(minVal, maxVal, bounds); 847 syncPriceFilterFields($table, range); 848 }); 849 } 850 851 function getPriceFilterBounds($table) { 852 if (!$table || !$table.length) return { min: 0, max: 0 }; 853 var min = parseFloat($table.data("priceMin")); 854 var max = parseFloat($table.data("priceMax")); 855 if (!isFinite(min)) min = 0; 856 if (!isFinite(max)) max = min; 857 if (max < min) max = min; 858 return { min: min, max: max }; 859 } 860 861 function normalizePriceRange(min, max, bounds, changedKey) { 862 if (!isFinite(min)) min = bounds.min; 863 if (!isFinite(max)) max = bounds.max; 864 if (min < bounds.min) min = bounds.min; 865 if (max > bounds.max) max = bounds.max; 866 if (min > max) { 867 if (changedKey === "min") { 868 min = max; 869 } else { 870 max = min; 871 } 872 } 873 return { min: min, max: max }; 874 } 875 876 function parsePriceValue(val) { 877 if (typeof val === "string") { 878 val = val.replace(",", "."); 879 } 880 var num = parseFloat(val); 881 return isFinite(num) ? num : NaN; 882 } 883 884 function updatePriceTrack($table, range) { 885 if (!$table || !$table.length || !range) return; 886 var bounds = getPriceFilterBounds($table); 887 var $fill = $table.find(".price-filter-track-fill"); 888 if (!$fill.length) return;
889 var minPerc = bounds.max === bounds.min ? 0 : ((range.min - bounds.min) / (bounds.max - bounds.min)) * 100; 890 var maxPerc = bounds.max === bounds.min ? 100 : ((range.max - bounds.min) / (bounds.max - bounds.min)) * 100; 891 if (!isFinite(minPerc)) minPerc = 0; 892 if (!isFinite(maxPerc)) maxPerc = 0; 893 if (minPerc < 0) minPerc = 0; 894 if (maxPerc > 100) maxPerc = 100; 895 if (maxPerc < minPerc) maxPerc = minPerc; 896 $fill.css({ 897 left: minPerc + "%", 898 width: (maxPerc - minPerc) + "%" 899 }); 900 } 901 902 function syncPriceFilterFields($table, range) { 903 if (!$table || !$table.length || !range) return; 904 $table.find(".price-filter-range-min").val(range.min); 905 $table.find(".price-filter-range-max").val(range.max); 906 $table.find(".price-filter-input-min").val(range.min); 907 $table.find(".price-filter-input-max").val(range.max); 908 updatePriceTrack($table, range); 909 } 910 911 function sendPriceFilterRequest(range) { 912 Cat.page = "1"; sv("#nowpage", "1"); 913 scheduleCatalogScrollTop(); 914 var p = payload(), value = valueMap(p, { nowpage: 1 }); 915 if (range && range.reset) { 916 pendingFiltersReset = true; 917 value.reset_price = 1; 918 } else { 919 if (range && range.min !== undefined && range.min !== null && range.min !== "" && !isNaN(parseFloat(range.min))) { 920 var minVal = parseFloat(range.min); 921 value.price_min = Number(minVal.toFixed ? minVal.toFixed(2) : minVal); 922 } 923 if (range && range.max !== undefined && range.max !== null && range.max !== "" && !isNaN(parseFloat(range.max))) { 924 var maxVal = parseFloat(range.max); 925 value.price_max = Number(maxVal.toFixed ? maxVal.toFixed(2) : maxVal); 926 } 927 } 928 fpLog("price filter request", { value: value }); 929 postJSON("updateFilters", { value: value }, function () { 930 refreshSessionFiltersState(); 931 renderCatalogThenFilters(p); 932 }); 933 } 934 935 function updateMobileFiltersResultCount() { 936 $(".mobile-filters-result-count").text(lastCatalogResultCount); 937 } 938 939 function refreshCatalogResultCount() { 940 var $catalog = $("#catalog"); 941 if (!$catalog.length) return; 942 //var totalAttr = parseInt($catalog.attr("data-results-total"), 10); 943 var totalAttr = parseInt($("#data-results-total").text(), 10); 944 var count = !isNaN(totalAttr) ? totalAttr : $catalog.find(".catalog-item").length; 945 lastCatalogResultCount = count || 0; 946 updateMobileFiltersResultCount(); 947 } 948 949 var shouldScrollCatalogTop = false; 950 var lastSessionFilters = null; 951 var pendingFiltersReset = false; 952 var filterAccordionUserPreference = {}; 953 954 function filterAccordionKey($element) { 955 if (!$element || !$element.length) return null; 956 var $table = $element.hasClass("filter-table") ? $element : findFilterTable($element); 957 if ($table && $table.length) { 958 var tableId = $table.attr("id"); 959 if (tableId) return tableId; 960 } 961 var id = $element.attr("id"); 962 if (id) return id; 963 return null; 964 } 965 966 function rememberFilterAccordionPreference($element, isOpen) { 967 var key = filterAccordionKey($element); 968 if (!key) return; 969 filterAccordionUserPreference[key] = !!isOpen; 970 } 971 972 function readFilterAccordionPreference($element) { 973 var key = filterAccordionKey($element); 974 if (!key) return null; 975 return Object.prototype.hasOwnProperty.call(filterAccordionUserPreference, key) 976 ? filterAccordionUserPreference[key] 977 : null; 978 } 979 980 function scheduleCatalogScrollTop() { 981 shouldScrollCatalogTop = true; 982 // Attempt immediate scroll when the back-to-top anchor exists to improve UX on fast responses 983 var $backTop = $(".cd-top"); 984 if ($backTop.length) { 985 $backTop.trigger("click"); 986 } else { 987 scrollToPageTopSmooth(); 988 } 989 } 990 991 function setFiltersActiveState(hasFilters) { 992 document.body.classList.toggle("catalog-filters-active", !!hasFilters); 993 } 994 995 function hasSessionFilters(filters) { 996 if (!filters || typeof filters !== "object") return false; 997 var keys = [ 998 "color_filters", 999 "sizes_filters", 1000 "composition_filters", 1001 "flavor_filters", 1002 "brand_filters", "caracteristicas_filters", 1003 "promo_filters", 1004 "stock_filters", 1005 "price_filters" 1006 ]; 1007 return keys.some(function (key) { 1008 if (!Object.prototype.hasOwnProperty.call(filters, key)) return false;
1009 var value = filters[key]; 1010 if (!value) return false; 1011 if (Array.isArray(value)) return value.length > 0; 1012 if (typeof value === "object") return Object.keys(value).length > 0; 1013 return !!value; 1014 }); 1015 } 1016 1017 function refreshSessionFiltersState(done) { 1018 var catKey = getFilterCategoryKey(); 1019 var req = postJSON("checkCurrentFilters", null, function (currentFilters) { 1020 lastSessionFilters = currentFilters || null; 1021 saveFiltersSnapshot(catKey, lastSessionFilters || {}); 1022 syncFiltersStateToUrl(lastSessionFilters || {}); 1023 setFiltersActiveState(hasSessionFilters(lastSessionFilters)); 1024 fpLog("refreshSessionFiltersState", { catKey: catKey, snapshot: lastSessionFilters }); 1025 if (typeof done === "function") done(currentFilters); 1026 }); 1027 if (req && typeof req.fail === "function") { 1028 req.fail(function () { 1029 if (typeof done === "function") done(null); 1030 }); 1031 } 1032 return req; 1033 } 1034 1035 function renderFilters(p, done) { 1036 var $ct = $("#menu-filters").length ? $("#menu-filters") : $("#filters"); 1037 var hasExtraQueryFilters = $("#extra_query_filters").length > 0; 1038 if (!hasExtraQueryFilters) { 1039 if ($ct.length) { 1040 $ct.empty(); 1041 } 1042 pendingFiltersReset = false; 1043 done && done(); 1044 return; 1045 } 1046 if (!$ct.length) { pendingFiltersReset = false; done && done(); return; } 1047 postHTML("generateFilterMenu", p, function (html) { 1048 var desktopHtml = buildDesktopFiltersMarkup(html); 1049 $ct.html(desktopHtml); 1050 var $desktopScope = $ct.find(".filters-panel-content"); 1051 if (!$desktopScope.length) { 1052 $desktopScope = $ct; 1053 } 1054 1055 var mobileFiltersHtml = buildMobileFiltersMarkup(html); 1056 var $mobileContainer = $("#mobile-filters-container"); 1057 if ($mobileContainer.length) { 1058 $mobileContainer.html(mobileFiltersHtml); 1059 prepareMobileFiltersState($mobileContainer); 1060 initPriceFilterTables($mobileContainer); 1061 } 1062 1063 var $mobileFiltersDiv = $("#mobilefilters-div"); 1064 if ($mobileFiltersDiv.length) { 1065 $mobileFiltersDiv.html(mobileFiltersHtml); 1066 prepareMobileFiltersState($mobileFiltersDiv); 1067 initPriceFilterTables($mobileFiltersDiv); 1068 } 1069 1070 openSelected($desktopScope); 1071 syncSizeFilterGrid($desktopScope); 1072 syncColorFilterGrid($desktopScope); 1073 initPriceFilterTables($desktopScope); 1074 var sessionStateKnown = lastSessionFilters && typeof lastSessionFilters === "object"; 1075 var hasFiltersSession = sessionStateKnown ? hasSessionFilters(lastSessionFilters) : false; 1076 var hasFiltersDom = !isFiltersEmpty(); 1077 var hasFilters = sessionStateKnown ? hasFiltersSession : hasFiltersDom; 1078 if (pendingFiltersReset) { 1079 hasFilters = false; 1080 pendingFiltersReset = false; 1081 } 1082 updateMobileFiltersBadge(hasFilters); 1083 updateMobileFiltersReset(hasFilters); 1084 updateDesktopFiltersReset(hasFilters); 1085 if (!sessionStateKnown) { 1086 setFiltersActiveState(hasFilters); 1087 } 1088 updateMobileFiltersResultCount(); 1089 done && done(); 1090 }); 1091 } 1092 1093 function isFiltersEmpty() { 1094 if ($(".filtercolor .fa-selected, .filtersize .fa-selected, .filtercomposition .fa-selected, .filterflavor .fa-selected, .filterbrand .fa-selected, .filterfeature .fa-selected, .filterpromo .fa-selected, .filterstock .fa-selected").length) { 1095 return false; 1096 } 1097 1098 var hasInputs = false; 1099 var filters = $("#menu-filters :input, #filters :input, .filter :input"); 1100 filters.each(function () { 1101 hasInputs = true; 1102 var $input = $(this); 1103 var inputType = ($input.attr("type") || "").toLowerCase(); 1104 if (inputType === "hidden") { 1105 return; // hidden helpers shouldn't count as active filters 1106 } 1107 if ($(this).is(":checkbox") || $(this).is(":radio")) { 1108 if ($(this).is(":checked")) { 1109 hasInputs = false; 1110 return false; 1111 } 1112 } else { 1113 var val = $(this).val(); 1114 if (val && String(val).trim() !== "") { 1115 hasInputs = false; 1116 return false; 1117 } 1118 } 1119 }); 1120 1121 var $priceTable = $("#price-filter-table"); 1122 if ($priceTable.length) { 1123 var bounds = getPriceFilterBounds($priceTable); 1124 var minVal = parsePriceValue($priceTable.find(".price-filter-input-min").val()); 1125 var maxVal = parsePriceValue($priceTable.find(".price-filter-input-max").val()); 1126 if (isFinite(minVal) && isFinite(maxVal)) { 1127 minVal = Number(minVal.toFixed(2)); 1128 maxVal = Number(maxVal.toFixed(2)); 1129 var minDefault = Number(bounds.min.toFixed(2)); 1130 var maxDefault = Number(bounds.max.toFixed(2)); 1131 if (minVal > minDefault || maxVal < maxDefault) { 1132 return false;
1133 } 1134 } 1135 } 1136 1137 return hasInputs; 1138 } 1139 1140 function updateMobileFiltersBadge(hasFilters) { 1141 var $btn = $("#mobile-filters-btn"); 1142 if (!$btn.length) return; 1143 if (hasFilters) $btn.addClass("mobile-filters-btn-active"); 1144 else $btn.removeClass("mobile-filters-btn-active"); 1145 } 1146 1147 function renderCatalog(p, done) { 1148 var $cat = $("#catalog"); 1149 if (!$cat.length) { 1150 if (typeof done === "function") done(); 1151 return; 1152 } 1153 $cat.removeClass("catalog-preloader").show(); 1154 postHTML("catalog", p, function (html) { 1155 $cat.html(html).removeClass("catalog-preloader").show(); 1156 if ($.fn.lazy) $(".lazy").Lazy(); 1157 if ($.fancybox) $('[data-fancybox]').fancybox({ loop: true }); 1158 bindEvents(); // rebind após injeção 1159 refreshCatalogResultCount(); 1160 if (shouldScrollCatalogTop) { 1161 shouldScrollCatalogTop = false; 1162 scrollToPageTopSmooth(); 1163 } 1164 if (typeof done === "function") done(); 1165 }); 1166 } 1167 1168 function renderCatalogThenFilters(p, done) { 1169 var hasCatalog = $("#catalog").length > 0; 1170 var finalize = function () { 1171 renderFilters(p, done); 1172 }; 1173 if (hasCatalog) { 1174 renderCatalog(p, finalize); 1175 } else { 1176 finalize(); 1177 } 1178 } 1179 1180 function scrollToPageTopSmooth() { 1181 if ("scrollBehavior" in document.documentElement.style) { 1182 window.scrollTo({ top: 0, behavior: "smooth" }); 1183 } else { 1184 $("html, body").stop().animate({ scrollTop: 0 }, 500); 1185 } 1186 } 1187 1188 function syncThenRender(opts) { 1189 var isReset = opts && opts.resetAll; 1190 if (isReset) pendingFiltersReset = true; 1191 var p = payload(), val = valueMap(p, isReset ? { reset_all: 1 } : null); 1192 var catKey = getFilterCategoryKey(); 1193 fpLog("syncThenRender", { catKey: catKey, reset: isReset, payload: val }); 1194 var renderAll = function () { 1195 renderCatalogThenFilters(p); 1196 }; 1197 var req = postJSON("updateFilters", { value: val }, function () { 1198 refreshSessionFiltersState(function (filters) { 1199 saveFiltersSnapshot(catKey, filters); 1200 rememberFilterCategory(catKey); 1201 renderAll(); 1202 }); 1203 }); 1204 if (req && typeof req.fail === "function") { 1205 req.fail(renderAll); 1206 } 1207 } 1208 1209 function toggleLiveSearch(mode) { 1210 var $search = $("#livesearch"); 1211 if (!$search.length) return; 1212 var $html = $("html"); 1213 var $reset = $("#livesearch-reset"); 1214 var $mainBlocks = $("#main-container, #main-content, .main-footer-banners-container, .main-footer-container"); 1215 var $topMenus = $("#topm-cats, #topm-info"); 1216 var $header = $(".webstore-header-container"); 1217 var $mobileLeft = $(".header-left-mobile-banner"); 1218 var $mobileRight = $(".header-right-mobile-banner"); 1219 var $toolsMobile = $("#tools-options-mobile"); 1220 1221 switch (mode) { 1222 case "hide": 1223 $html.css({ overflow: "auto" }).off("touchmove.livesearch"); 1224 $search.scrollTop(0).hide().empty(); 1225 $("#q").val(""); 1226 $reset.hide(); 1227 $toolsMobile.hide(); 1228 $mainBlocks.show(); 1229 $topMenus.removeClass("main-container-search"); 1230 $header.removeClass("webstore-header-container-open"); 1231 if ($("#hamburguer").is(":visible")) { 1232 $mobileLeft.show(); 1233 $mobileRight.show(); 1234 } 1235 break; 1236 case "show": 1237 $mainBlocks.hide(); 1238 $toolsMobile.hide(); 1239 $topMenus.addClass("main-container-search"); 1240 $header.addClass("webstore-header-container-open"); 1241 if ($("#hamburguer").is(":visible")) { 1242 $mobileLeft.hide(); 1243 $mobileRight.hide(); 1244 } 1245 $reset.show().css({ display: "inline-block" }); 1246 $search.show().css({ overflow: "auto" }); 1247 $html.css({ overflow: "hidden" }).on("touchmove.livesearch", function (ev) { 1248 if (!$(ev.target).closest("#livesearch").length) ev.preventDefault(); 1249 }); 1250 break; 1251 case "reset": 1252 default: 1253 $html.css({ overflow: "auto" }).off("touchmove.livesearch"); 1254 $mainBlocks.show(); 1255 $toolsMobile.hide(); 1256 $topMenus.removeClass("main-container-search"); 1257 $header.removeClass("webstore-header-container-open"); 1258 if ($("#hamburguer").is(":visible")) { 1259 $mobileLeft.show(); 1260 $mobileRight.show(); 1261 } 1262 $reset.hide(); 1263 $search.hide().empty(); 1264 break; 1265 } 1266 } 1267 1268 /* ========================================================== 1269 * 5) EVENTOS (namespaces para evitar double-binds) 1270 * ========================================================== */ 1271 function goToPage(n, push) { 1272 Cat.page = String(Math.max(1, parseInt(n, 10) || 1)); sv("#nowpage", Cat.page); 1273 if (push === true) pushPage(Cat.page); 1274 syncThenRender(); 1275 } 1276 1277 // Mapa de normalização de chaves para o endpoint updateFilters 1278 var KEYMAP = { 1279 filtercolor: 'color', 1280 filtersize: 'size', 1281 filtercomposition: 'composition', 1282 filterbrand: 'brand', 1283 filterflavor: 'flavor', 1284 filterfeature: 'feature', 1285 filterpromo: 'promo', 1286 filterstock: 'stock', 1287 filteritem: 'item' // se existir 1288 }; 1289 function kmap(k){ return KEYMAP[k] || k; } 1290 1291 /* ========================================================== 1292 * EVENT BINDINGS (dispatcher) 1293 * ========================================================== */ 1294 function bindEvents() { 1295 bindHeaderNavigation(); 1296 bindFavoritesFeature(); 1297 bindCurrencySwitcher(); 1298 bindCatalogControls(); 1299 bindFilterInteractions(); 1300 bindMenuToggles(); 1301 bindScrollHelpers(); 1302 enableMobileStickyNav(); 1303 bindProductSizeSelector(); 1304 bindProductAddToCart(); 1305 bindProductTabsNavigation();
1306 bindCartQuantityControls(); 1307 bindDiscountHandlers(); 1308 bindCheckoutFlowEvents(); 1309 bindDeliveryApiLanguageSwitch(); 1310 bindGiftAndInfoToggles(); 1311 bindLiveSearchEvents(); 1312 bindCartRemovalEvents(); 1313 initOrderForm(); 1314 } 1315 1316 function bindHeaderNavigation() { 1317 $(document) 1318 .off("click.cartNav") 1319 .on("click.cartNav", ".shopcart-container > .shopcart-selector, .shopcart-container .shopcart-badge, .shopcart-selector-small", function (e) { 1320 e.stopPropagation(); // um so open por clique (badge e selector partilham o container) 1321 e.preventDefault(); 1322 if (typeof window.openShopcartDrawer === "function") { 1323 window.openShopcartDrawer(); 1324 return; 1325 } 1326 var lang = Cookies.get("clang_id") || "pt"; 1327 window.location.assign("/" + lang + "/cart/"); 1328 }); 1329 1330 $(document) 1331 .off("click.cartNavModal") 1332 .on("click.cartNavModal", ".fichaGotoCartModal, .fichaGotoCart", function (e) { 1333 e.preventDefault(); 1334 if (typeof window.openShopcartDrawer === "function") { 1335 window.openShopcartDrawer(); 1336 return; 1337 } 1338 var lang = Cookies.get("clang_id") || "pt"; 1339 window.location.assign("/" + lang + "/checkout/"); 1340 }); 1341 1342 $(document) 1343 .off("click.logout") 1344 .on("click.logout", ".logout", function (e) { 1345 e.preventDefault(); 1346 logoutUser(); 1347 }); 1348 } 1349 1350 function bindFavoritesFeature() { 1351 var favoritesListsEnabled = !!(hasWindow && window.CUPIDO_FAVORITES_LISTS_ENABLED); 1352 1353 function favoritesModalMarkup() { 1354 return [ 1355 "<div class='favorites-lists-modal' id='favoritesListsModal'>", 1356 " <div class='favorites-lists-modal__dialog'>", 1357 " <button type='button' class='favorites-lists-modal__close' aria-label='Fechar'><i class='fa fa-times'></i></button>", 1358 " <div class='favorites-lists-modal__view is-active' data-view='lists'>", 1359 " <h3 class='favorites-lists-modal__title'>Gerir favoritos</h3>", 1360 " <button type='button' class='favorites-lists-modal__create-link'>Criar nova lista</button>", 1361 " <div class='favorites-lists-modal__lists'></div>", 1362 " </div>", 1363 " <div class='favorites-lists-modal__view' data-view='create'>", 1364 " <div class='favorites-lists-modal__topline'><button type='button' class='favorites-lists-modal__back'><i class='fa fa-arrow-left'></i></button><h3 class='favorites-lists-modal__title'>Criar nova lista</h3></div>", 1365 " <input type='text' class='favorites-lists-modal__input' placeholder='Nome da lista'>", 1366 " <button type='button' class='favorites-lists-modal__submit'>Criar lista</button>", 1367 " </div>", 1368 " </div>", 1369 "</div>" 1370 ].join(""); 1371 } 1372 1373 function ensureFavoritesModal() { 1374 if (!favoritesListsEnabled) return $(); 1375 var $modal = $("#favoritesListsModal"); 1376 if ($modal.length) return $modal; 1377 $("body").append(favoritesModalMarkup()); 1378 if (!$("#favoritesListsModalStyles").length) { 1379 $("head").append([ 1380 "<style id='favoritesListsModalStyles'>", 1381 ".favorites-lists-modal{position:fixed;inset:0;z-index:99999;background:rgba(15,23,42,.45);display:none;align-items:center;justify-content:center;padding:20px}", 1382 ".favorites-lists-modal.is-open{display:flex}", 1383 ".favorites-lists-modal__dialog{width:min(100%,560px);max-height:min(90vh,720px);overflow:auto;background:#fff;border-radius:24px;padding:22px 22px 18px;box-shadow:0 30px 80px rgba(15,23,42,.28);position:relative}", 1384 ".favorites-lists-modal__close{position:absolute;top:16px;right:16px;width:40px;height:40px;border:0;background:#fff;color:#94a3b8;font-size:22px;cursor:pointer}", 1385 ".favorites-lists-modal__title{margin:0 0 18px;font:700 24px 'Raleway',sans-serif;color:#1f2937}", 1386 ".favorites-lists-modal__create-link{padding:0;border:0;background:none;color:#3b82f6;font:500 18px 'Raleway',sans-serif;cursor:pointer;margin-bottom:12px}", 1387 ".favorites-lists-modal__lists{display:flex;flex-direction:column;border-top:1px solid #e5e7eb}", 1388 ".favorites-lists-modal__item{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 4px;border-bottom:1px solid #e5e7eb;cursor:pointer}", 1389 ".favorites-lists-modal__item.is-selected{background:#eff6ff}", 1390 ".favorites-lists-modal__item-left{display:flex;align-items:center;gap:14px;min-width:0}", 1391 ".favorites-lists-modal__icon{font-size:32px;color:#111827;min-width:34px;text-align:center}", 1392 ".favorites-lists-modal__name{font:600 18px 'Raleway',sans-serif;color:#1f2937}", 1393 ".favorites-lists-modal__check{font-size:28px;color:#111827;min-width:28px;text-align:center}", 1394 ".favorites-lists-modal__view{display:none}", 1395 ".favorites-lists-modal__view.is-active{display:block}", 1396 ".favorites-lists-modal__topline{display:flex;align-items:center;gap:14px;margin-bottom:18px}", 1397 ".favorites-lists-modal__back{padding:0;border:0;background:none;font-size:28px;color:#111827;cursor:pointer}", 1398 ".favorites-lists-modal__input{width:100%;height:58px;padding:0 16px;border:2px solid #3b82f6;border-radius:14px;font:500 18px 'Raleway',sans-serif;color:#1f2937;box-sizing:border-box}", 1399 ".favorites-lists-modal__submit{margin-top:16px;width:100%;height:58px;border:0;border-radius:14px;background:#2975f0;color:#fff;font:700 20px 'Raleway',sans-serif;cursor:pointer}", 1400 "@media (max-width:640px){.favorites-lists-modal{padding:12px}.favorites-lists-modal__dialog{border-radius:20px;padding:18px 18px 16px}.favorites-lists-modal__title{font-size:21px}.favorites-lists-modal__name{font-size:17px}}", 1401 "</style>" 1402 ].join("")); 1403 } 1404 return $("#favoritesListsModal"); 1405 } 1406 1407 function setFavoriteVisual($btn, hasFavorites) { 1408 var isCatalog = $btn.hasClass("catalog_favorite_add") || $btn.hasClass("catalog_favorite_remove"); 1409 var isLive = $btn.hasClass("livesearch_favorite_add") || $btn.hasClass("livesearch_favorite_remove"); 1410 if (isCatalog) { 1411 if (hasFavorites) { 1412 $btn.removeClass("catalog_favorite_add far fa-heart-o").addClass("catalog_favorite_remove fa fa-heart"); 1413 } else { 1414 $btn.removeClass("catalog_favorite_remove fa fa-heart").addClass("catalog_favorite_add far fa-heart-o"); 1415 } 1416 return; 1417 } 1418 if (isLive) { 1419 if (hasFavorites) { 1420 $btn.removeClass("livesearch_favorite_add fa-heart-o far").addClass("livesearch_favorite_remove fa fa-heart"); 1421 } else { 1422 $btn.removeClass("livesearch_favorite_remove fa fa-heart").addClass("livesearch_favorite_add far fa-heart-o"); 1423 } 1424 return; 1425 } 1426 if ($btn.is("input")) { 1427 if (hasFavorites) { 1428 $btn.removeClass("shopcart_favorite_add catalog_favorite_add").addClass("shopcart_favorite_remove btnFavoritesSelected").val(gl("BTN_FICHA_REMOVER_FAVORITOS") || "Remover favorito"); 1429 } else { 1430 $btn.removeClass("shopcart_favorite_remove catalog_favorite_remove btnFavoritesSelected").addClass("shopcart_favorite_add").val(gl("BTN_FICHA_ADD_FAVORITOS") || "Adicionar favorito"); 1431 } 1432 return; 1433 } 1434 if (hasFavorites) { 1435 $btn.removeClass("shopcart_favorite_add catalog_favorite_add").addClass("shopcart_favorite_remove btnFavoritesSelected").html("<i class='fas fa-heart fa-2x'></i><br>" + (gl("LIVES_REM_FAVORITE") || "Remover favorito")); 1436 } else { 1437 $btn.removeClass("shopcart_favorite_remove catalog_favorite_remove btnFavoritesSelected").addClass("shopcart_favorite_add").html("<i class='far fa-heart fa-2x'></i><br>" + (gl("LIVES_ADD_FAVORITE") || "Adicionar favorito")); 1438 } 1439 } 1440 1441 function getFavoriteContext($btn) { 1442 var productId = 1443 $btn.data("id_code") || 1444 $btn.attr("id_code") || 1445 $btn.data("id") || 1446 $btn.attr("data-id") || 1447 $btn.attr("rel") || 1448 $("#id_code").val() || 1449 ""; 1450 var elementId = 0; 1451 var catalogElements = parseInt($btn.attr("elementos") || $btn.data("elementos") || "0", 10) || 0; 1452 var hasSizeSelector = $("#tabela_tamanhos .sizebox").length > 0; 1453 var $activeSize = $("#tabela_tamanhos .sizebox.size_btn_active:first"); 1454 if ($activeSize.length) { 1455 elementId = parseInt($activeSize.attr("rel") || $activeSize.data("id") || $activeSize.data("rel") || "0", 10) || 0; 1456 } 1457 return { 1458 productId: productId, 1459 elementId: elementId, 1460 requiresVariant: hasSizeSelector || catalogElements > 0, 1461 hasVariantSelected: elementId > 0 1462 }; 1463 } 1464 1465 function syncFavoriteButtons(productId, hasFavorites) { 1466 var selector = "[id_code='" + productId + "'],[data-id_code='" + productId + "'],[data-id='" + productId + "'],[rel='" + productId + "']"; 1467 $(selector).filter(".shopcart_favorite_add, .shopcart_favorite_remove, #shopcart_favorite_add, #shopcart_favorite_remove, .catalog_favorite_add, .catalog_favorite_remove, .btnFavorites, .livesearch_favorite_add, .livesearch_favorite_remove").each(function () { 1468 setFavoriteVisual($(this), hasFavorites); 1469 }); 1470 } 1471 1472 function refreshFavoritesFragments() { 1473 if (typeof window.favoritesMenu === "function") window.favoritesMenu(); 1474 if (typeof window.favoritesSHreminder === "function") window.favoritesSHreminder(); 1475 if (typeof window.loadFavoritesListsPanel === "function") window.loadFavoritesListsPanel(); 1476 } 1477 1478 function renderFavoritesLists($modal, response) { 1479 var selected = response && response.selected_ids ? response.selected_ids : []; 1480 var lists = response && response.lists ? response.lists : []; 1481 var html = ""; 1482 if (!lists.length) { 1483 html = "<div class='favorites-lists-modal__item'><div class='favorites-lists-modal__name'>Ainda não tem listas criadas.</div></div>"; 1484 } else { 1485 $.each(lists, function (_, item) { 1486 var isSelected = $.inArray(String(item.id_lista), $.map(selected, String)) !== -1; 1487 html += "<div class='favorites-lists-modal__item" + (isSelected ? " is-selected" : "") + "' data-list-id='" + item.id_lista + "'>" + 1488 "<div class='favorites-lists-modal__item-left'><i class='fa fa-bookmark favorites-lists-modal__icon'></i><span class='favorites-lists-modal__name'>" + $("<div>").text(item.nome_lista).html() + "</span></div>" + 1489 "<div class='favorites-lists-modal__check'>" + (isSelected ? "<i class='fa fa-check'></i>" : "") + "</div></div>"; 1490 }); 1491 } 1492 $modal.find(".favorites-lists-modal__lists").html(html); 1493 } 1494 1495 function openFavoritesListsModal(productId, elementId) { 1496 var $modal = ensureFavoritesModal(); 1497 if (!$modal.length) return; 1498 $modal.data("product-id", productId).data("element-id", elementId || 0); 1499 $modal.find(".favorites-lists-modal__view").removeClass("is-active"); 1500 $modal.find("[data-view='lists']").addClass("is-active"); 1501 $modal.find(".favorites-lists-modal__lists").html("<div class='favorites-lists-modal__item'><div class='favorites-lists-modal__name'>A carregar...</div></div>"); 1502 $.post(REQUEST_ENDPOINT, { ajax: "favorites_lists_modal", id_produto: productId, id_produto_elemento: elementId || 0 }, function (response) { 1503 if (response && response.message === "login_required") { 1504 closeFavoritesListsModal(); 1505 var lang = ($("#session_lang").val() || "pt"); 1506 fetchLangDict().always(function () { 1507 swal({ 1508 title: gl("CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TIT
1508LE") || "Listas e Favoritos", 1509 text: gl("CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TEXT") || "Para usar a funcionalidade de Listas e Favoritos, por favor faça login ou registe-se", 1510 type: "info", 1511 showCancelButton: true, 1512 confirmButtonColor: "#abd559", 1513 cancelButtonColor: "#d9dee7", 1514 confirmButtonText: gl("CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_REGISTER") || "Registar", 1515 cancelButtonText: gl("swal_btnok") || "OK", 1516 closeOnConfirm: true, 1517 closeOnCancel: true, 1518 animation: false 1519 }, function (isConfirm) { 1520 if (isConfirm) { 1521 window.location.href = "/" + lang + "/account/"; 1522 } 1523 }); 1524 }); 1525 return; 1526 } 1527 if (!response || !response.ok) { 1528 toastr.error("Não foi possÃvel carregar as listas."); 1529 return; 1530 } 1531 $modal.addClass("is-open"); 1532 renderFavoritesLists($modal, response); 1533 $modal.data("has-favorites", !!response.has_favorites); 1534 syncFavoriteButtons(productId, !!response.has_favorites); 1535 }, "json").fail(function () { 1536 closeFavoritesListsModal(); 1537 toastr.error("Não foi possÃvel carregar as listas."); 1538 }); 1539 } 1540 1541 function closeFavoritesListsModal() { 1542 $("#favoritesListsModal").removeClass("is-open"); 1543 } 1544 1545 if (favoritesListsEnabled) { 1546 $(document) 1547 .off("click.favoritesAll") 1548 .on("click.favoritesAll", 1549 ".shopcart_favorite_add, .shopcart_favorite_remove, #shopcart_favorite_add, #shopcart_favorite_remove, .catalog_favorite_add, .catalog_favorite_remove, .btnFavorites, .livesearch_favorite_add, .livesearch_favorite_remove", 1550 function (e) { 1551 e.preventDefault(); 1552 e.stopImmediatePropagation(); 1553 var $btn = $(this); 1554 var ctx = getFavoriteContext($btn); 1555 if (!ctx.productId) { 1556 toastr.error("Não consegui identificar o produto."); 1557 return; 1558 } 1559 if (ctx.requiresVariant && !ctx.hasVariantSelected) { 1560 toastr.warning("Selecione a opção pretendida antes de guardar nos favoritos."); 1561 return; 1562 } 1563 openFavoritesListsModal(ctx.productId, ctx.elementId); 1564 }); 1565 1566 $(document) 1567 .off("click.favmenuRemove") 1568 .on("click.favmenuRemove", ".favmenu-remove", function (e) { 1569 e.preventDefault(); 1570 var id_code = $(this).data("id_code") || $(this).attr("id_code") || $(this).attr("rel") || ""; 1571 if (!id_code) return; 1572 $.post(REQUEST_ENDPOINT, { ajax: "favorites_lists_remove_all_product", id_produto: id_code, id_produto_elemento: 0 }, function () { 1573 syncFavoriteButtons(id_code, false); 1574 refreshFavoritesFragments(); 1575 toastr.info(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos"); 1576 }, "json"); 1577 }); 1578 1579 $(document) 1580 .off("click.favoritesModalClose") 1581 .on("click.favoritesModalClose", ".favorites-lists-modal__close, .favorites-lists-modal__back", function () { 1582 var $modal = $("#favoritesListsModal"); 1583 if ($(this).hasClass("favorites-lists-modal__back")) { 1584 $modal.find(".favorites-lists-modal__view").removeClass("is-active"); 1585 $modal.find("[data-view='lists']").addClass("is-active"); 1586 return; 1587 } 1588 closeFavoritesListsModal(); 1589 }) 1590 .off("click.favoritesModalOverlay") 1591 .on("click.favoritesModalOverlay", "#favoritesListsModal", function (e) { 1592 if (e.target === this) closeFavoritesListsModal(); 1593 }) 1594 .off("click.favoritesModalCreateLink") 1595 .on("click.favoritesModalCreateLink", ".favorites-lists-modal__create-link", function () { 1596 var $modal = $("#favoritesListsModal"); 1597 $modal.find(".favorites-lists-modal__view").removeClass("is-active"); 1598 $modal.find("[data-view='create']").addClass("is-active"); 1599 $modal.find(".favorites-lists-modal__input").val("").focus(); 1600 }) 1601 .off("click.favoritesModalSubmit") 1602 .on("click.favoritesModalSubmit", ".favorites-lists-modal__submit", function () { 1603 var $modal = $("#favoritesListsModal"); 1604 var productId = $modal.data("product-id"); 1605 var elementId = $modal.data("element-id") || 0; 1606 var name = $.trim($modal.find(".favorites-lists-modal__input").val()); 1607 if (!name) return; 1608 $.post(REQUEST_ENDPOINT, { ajax: "favorites_lists_create", nome_lista: name, id_produto: productId, id_produto_elemento: elementId }, function (response) { 1609 if (!response || !response.ok) { 1610 toastr.error("Não foi possÃvel criar a lista."); 1611 return; 1612 } 1613 $modal.find(".favorites-lists-modal__view").removeClass("is-active"); 1614 $modal.find("[data-view='lists']").addClass("is-active"); 1615 renderFavoritesLists($modal, response); 1616 syncFavoriteButtons(productId, !!response.has_favorites); 1617 refreshFavoritesFragments(); 1618 }, "json"); 1619 }) 1620 .off("click.favoritesModalItem") 1621 .on("click.favoritesModalItem", ".favorites-lists-modal__item[data-list-id]", function () { 1622 var $item = $(this); 1623 var $modal = $("#favoritesListsModal"); 1624 var productId = $modal.data("product-id"); 1625 var elementId = $modal.data("element-id") || 0; 1626 var listId = $item.data("list-id"); 1627 var selected = !$item.hasClass("is-selected"); 1628 $.post(REQUEST_ENDPOINT, { ajax: "favorites_lists_toggle_product", id_lista: listId, id_produto: productId, id_produto_elemento: elementId, selected: selected ? 1 : 0 }, function (response) { 1629 if (!response || !response.ok) { 1630 toastr.error("Não foi possÃvel atualizar os favoritos."); 1631 return; 1632 } 1633 renderFavoritesLists($modal, response); 1634 syncFavoriteButtons(productId, !!response.has_favorites); 1635 refreshFavoritesFragments(); 1636 toastr[selected ? "success" : "info"](selected ? (gl("LIVES_ADD_FAVORITE_INFO") || "Adicionado aos favoritos") : (gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos")); 1637 }, "json"); 1638 }); 1639 1640 window.favoritesMenu = function () { 1641 $.post(REQUEST_ENDPOINT, { ajax: "favoritesMenu" }, function (html) { 1642 $(".favorites-menu, #favorites-menu, .divfavorites-menu").html(html); 1643 if ($.fn.lazy) $(".lazy").Lazy(); 1644 }); 1645 }; 1646 return; 1647 } 1648 1649 $(document) 1650 .off("click.favoritesAll") 1651 .on("click.favoritesAll", 1652 ".shopcart_favorite_add, .shopcart_favorite_remove, " + 1653 ".catalog_favorite_add, .catalog_favorite_remove, " + 1654 ".btnFavorites", 1655 function (e) { 1656 e.preventDefault(); 1657 1658 var $btn = $(this); 1659 var id_code = 1660 $btn.data("id_code") || 1661 $btn.attr("id_code") || 1662 $btn.data("id") || 1663 $btn.attr("data-id") || 1664 $btn.attr("rel") || 1665 $("#id_code").val() || 1666 ""; 1667 1668 if (!id_code) { 1669 toastr.error("Não consegui identificar o produto."); 1670 return; 1671 } 1672 1673 var isRemove = 1674 $btn.hasClass("shopcart_favorite_remove") || 1675 $btn.hasClass("catalog_favorite_remove") || 1676 $btn.hasClass("btnFavoritesSelected"); 1677 1678 var action = isRemove ? "shopcart_favorites_remove" : "shopcart_favorites_add"; 1679 var isCatalog = $btn.hasClass("catalog_favorite_add") || $btn.hasClass("catalog_favorite_remove"); 1680 1681 $.ajax({ 1682 type: "POST", 1683 url: REQUEST_ENDPOINT, 1684 data: { ajax: action, id_code: id_code }, 1685 success: function () { 1686 if (isCatalog) { 1687 if (action === "shopcart_favorites_add") { 1688 toastr.info(gl("LIVES_ADD_FAVORITE_INFO") || "Adicionado aos favoritos"); 1689 $btn.removeClass("catalog_favorite_add far fa-heart").addClass("catalog_favorite_remove fa fa-heart"); 1690 } else { 1691 toastr.warning(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos"); 1692 $btn.removeClass("catalog_favorite_remove fa fa-heart").addClass("catalog_favorite_add far fa-heart"); 1693 } 1694 } else { 1695 if (action === "shopcart_favorites_add") { 1696 toastr.success(gl("LIVES_ADD_FAVORITE_INFO") || "Adicionado aos favoritos"); 1697 $btn 1698 .removeClass("shopcart_favorite_add catalog_favorite_add") 1699 .addClass("shopcart_favorite_remove btnFavoritesSelected") 1700 .html("<i class='fas fa-heart fa-2x'></i><br>" + (gl("LIVES_REM_FAVORITE") || "Remover favorito")); 1701 } else { 1702 toastr.info(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos"); 1703 $btn 1704 .removeClass("shopcart_favorite_remove catalog_favorite_remove btnFavoritesSelected") 1705 .addClass("shopcart_favorite_add") 1706 .html("<i class='far fa-heart fa-2x'></i><br>" + (gl("LIVES_ADD_FAVORITE") || "Adicionar favorito")); 1707 } 1708 } 1709 1710 if (typeof window.favoritesMenu === "function") { 1711 window.favoritesMenu(); 1712 } else { 1713 $.post(REQUEST_ENDPOINT, { ajax: "favoritesMenu" }, function (html) { 1714 $(".favorites-menu, #favorites-menu, .divfavorites-menu").html(html); 1715 if ($.fn.lazy) $(".lazy").Lazy(); 1716 }); 1717 } 1718 } 1719 }); 1720 }); 1721 1722 $(document) 1723 .off("click.favmenuRemove") 1724 .on("click.favmenuRemove", ".favmenu-remove", function (e) { 1725 e.preventDefault(); 1726 var id_code = $(this).data("id_code") || $(this).attr("id_code") || $(this).attr("rel") || ""; 1727 if (!id_code) return; 1728 1729 $.post(REQUEST_ENDPOINT, { ajax: "shopcart_favorites_remove", id_code: id_code }, function () { 1730 toastr.info(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos"); 1731 1732 if (typeof window.favoritesMenu === "function") { 1733 window.favoritesMenu(); 1734 } 1735 1736 if (typeof window.favoritesSHreminder === "function") { 1737 window.favoritesSHreminder(); 1738 } 1739 }); 1740 }); 1741 1742 window.favoritesMenu = function () { 1743 $.post(REQUEST_ENDPOINT, { ajax: "favoritesMenu" }, function (html) { 1744 $(".favorites-menu, #favorites-menu, .divfavorites-menu").html(html); 1745 if ($.fn.lazy) $(".lazy").Lazy(); 1746 }); 1747 }; 1748 } 1749 1750 function bindCurrencySwitcher() { 1751 $(document) 1752 .off("click.cur") 1753 .on("click.cur", ".tools-price", function (e) { 1754 e.preventDefault(); 1755 1756 var raw = (this.dataset.cur || $(this).attr("href") || $(this).text() || "").toLowerCase().trim(); 1757 var MAP = { 1758 'usd': 'usd', 'dolar': 'usd', 'dólar': 'usd', 1759 'gbp': 'libras', 'libras': 'libras', 'libra': 'libras', 1760 'eur': 'euros', 'euro': 'euros', 'euros': 'euros', 1761 'brl': 'reais', 'real': 'reais', 'reais': 'reais' 1762 }; 1763 1764 var cur = MAP[raw] || 'euros'; 1765 $('#tools-options').fadeOut(80); 1766 window.location.assign('/?cur=' + cur); 1767 }); 1768 } 1769 $(document) 1770 .off("click.mobileTools") 1771 .on("click.mobileTools", "#mobile-tools-selector", function (e) { 1772 e.preventDefault(); 1773 $("#tools-options-mobile").toggle(); 1774 }); 1775 1776 function bindCatalogControls() { 1777 $(document).off(".cat"); 1778 if (!Cat.order) { 1779 Cat.order = $(".webstore-order:first").val() || ""; 1780 } 1781 1782 $(document).on("change.cat", ".webstore-order", function () { 1783 Cat.order = $(this).val() || "1"; 1784 scheduleCatalogScrollTop(); 1785 goToPage(1, false); 1786 }); 1787 1788 $(document).on("change.cat", ".webstore-results-num", function () { 1789 Cat.perPage = $(this).val() || "32"; 1790 scheduleCatalogScrollTop(); 1791 goToPage(1, false); 1792 }); 1793 1794 $(document).on("change.cat", ".webstore-direct-page, .webstore-page-num", function () { 1795 goToPage($(this).val(), true); 1796 }); 1797 1798 $(document).on("click.cat", ".webstore-nav, #next-catalog-page, #prev-catalog-page, [data-page]", function (e) { 1799 e.preventDefault(); 1800 var targetPage = $(this).attr("page") || $(this).data("page"); 1801 if (targetPage) { 1802 scheduleCatalogScrollTop(); 1803 goToPage(targetPage, true); 1804 } 1805 }); 1806 } 1807 1808 function findFilterTable($header) { 1809 if (!$header || !$header.length) return $(); 1810 var $table = $header.next(".filter-table"); 1811 if (!$table.length) { 1812 $table = $header.nextAll(".filter-table").first(); 1813 } 1814 return $table; 1815 } 1816 1817 function findFilterHeader($table) { 1818 if (!$table || !$table.length) return $(); 1819 return $table.prevAll("[id$='-filter-header'], .sub_filters_container").first(); 1820 } 1821 1822 function setFilterAccordionState($target, shouldOpen, options) { 1823 if (!$target || !$target.length) return; 1824 var $header, $table; 1825 if ($target.hasClass("filter-table")) { 1826 $table = $target; 1827 $header = findFilterHeader($table); 1828 } else { 1829 $header = $target; 1830 $table = findFilterTable($header); 1831 } 1832 if (!$table || !$table.length) return; 1833 if ($header && $header.length) { 1834 $header 1835 .toggleClass("is-open", shouldOpen) 1836 .attr("aria-expanded", shouldOpen ? "true" : "false"); 1837 } 1838 $table.toggleClass("is-open", shouldOpen).css("display", ""); 1839 if (options && options.persist) { 1840 rememberFilterAccordionPreference($header && $header.length ? $header : $table, shouldOpen); 1841 } 1842 } 1843 1844 function toggleFilterAccordion($header) { 1845 if (!$header || !$header.length) return; 1846 var $table = findFilterTable($header); 1847 if (!$table.length) return; 1848 var willOpen = !$table.hasClass("is-open"); 1849 setFilterAccordionState($header, willOpen, { persist: true }); 1850 } 1851 1852 function bindFilterInteractions() { 1853 $(document) 1854 .off("click.catFilters") 1855 .on("click.catFilters", 1856 "#color-filter-header, #size-filter-header, #composition-filter-header, #brand-filter-header, #feature-filter-header, #flavor-filter-header, #price-filter-header", 1857 function (e) { 1858 e.preventDefault(); 1859 toggleFilterAccordion($(this)); 1860 }); 1861 1862 $(document) 1863 .off("click.catFiltersResetDesktop") 1864 .on("click.catFiltersResetDesktop", ".filters-panel-reset", function (e) { 1865 if ($(this).prop("disabled")) return; 1866 e.preventDefault(); 1867 pendingFiltersReset = true; 1868 updateDesktopFiltersReset(false); 1869 updateMobileFiltersReset(false); 1870 Cat.page = "1"; sv("#nowpage", "1"); 1871 scheduleCatalogScrollTop(); 1872 syncThenRender({ resetAll: true }); 1873 }); 1874 1875 $(document) 1876 .off("click.catFilterToggle") 1877 .on("click.catFilterToggle", ".filtercolor, .filtersize, .filtercomposition, .filterflavor, .filterbrand, .filterfeature, .filterpromo, .filterstock", function (e) { 1878 e.preventDefault(); 1879 var $a = $(this); 1880 var keyRaw = $a.data("key") || 1881 ($a.hasClass("filtercolor") ? "filtercolor" : 1882 $a.hasClass("filtersize") ? "filtersize" : 1883 $a.hasClass("filtercomposition") ? "filtercomposition" : 1884 $a.hasClass("filterbrand") ? "filterbrand" : 1885 $a.hasClass("filterflavor") ? "filterflavor" : 1886 $a.hasClass("filterfeature") ? "filterfeature" : 1887 $a.hasClass("filterpromo") ? "filterpromo" : 1888 $a.hasClass("filterstock") ? "filterstock" : null); 1889 1890 var key = kmap(keyRaw); 1891 var val = $a.data("value") || $a.attr("value") || $a.attr("id") || $.trim($a.text()); 1892 1893 Cat.page = "1"; sv("#nowpage", "1"); 1894 scheduleCatalogScrollTop(); 1895 var p = payload(), value = valueMap(p, { nowpage: 1 }); 1896 value[key] = val; 1897 1898 $.ajax({ 1899 type: "POST", 1900 url: REQUEST_ENDPOINT, 1901 data: { ajax: "updateFilters", value: value }, 1902 success: function () { 1903 refreshSessionFiltersState(); 1904 renderCatalogThenFilters(p); 1905 } 1906 }); 1907 }); 1908 1909 $(document).on("change.cat", "#menu-filters :input
1909, #filters :input, .filter :input", function () { 1910 Cat.page = "1"; sv("#nowpage", "1"); 1911 scheduleCatalogScrollTop(); 1912 var p = payload(), value = valueMap(p, { nowpage: 1 }); 1913 $("#menu-filters :input, #filters :input, .filter :input").each(function () { 1914 if (!this.name) return; 1915 var name = kmap(this.name.replace(/^value\[|\]$/g, "")); 1916 value[name] = $(this).is(":checkbox") 1917 ? ($(this).is(":checked") ? ($(this).val() || 1) : 0) 1918 : $(this).val(); 1919 }); 1920 postJSON("updateFilters", { value: value }, function () { 1921 refreshSessionFiltersState(); 1922 renderCatalogThenFilters(p); 1923 }); 1924 }); 1925 1926 $(document) 1927 .off("change.priceFilterRange") 1928 .on("change.priceFilterRange", ".price-filter-range", function () { 1929 var $table = $(this).closest(".price-filter-table"); 1930 if (!$table.length) return; 1931 var bounds = getPriceFilterBounds($table); 1932 var minVal = parsePriceValue($table.find(".price-filter-range-min").val()); 1933 var maxVal = parsePriceValue($table.find(".price-filter-range-max").val()); 1934 var range = normalizePriceRange(minVal, maxVal, bounds, $(this).hasClass("price-filter-range-min") ? "min" : "max"); 1935 syncPriceFilterFields($table, range); 1936 fpLog("price slider change", range); 1937 sendPriceFilterRequest(range); 1938 }); 1939 1940 $(document) 1941 .off("change.priceFilterInput") 1942 .on("change.priceFilterInput", ".price-filter-input", function () { 1943 var $table = $(this).closest(".price-filter-table"); 1944 if (!$table.length) return; 1945 var bounds = getPriceFilterBounds($table); 1946 var minVal = parsePriceValue($table.find(".price-filter-input-min").val()); 1947 var maxVal = parsePriceValue($table.find(".price-filter-input-max").val()); 1948 var range = normalizePriceRange(minVal, maxVal, bounds, $(this).hasClass("price-filter-input-min") ? "min" : "max"); 1949 syncPriceFilterFields($table, range); 1950 fpLog("price input change", range); 1951 sendPriceFilterRequest(range); 1952 }); 1953 1954 $(document) 1955 .off("click.priceFilterApply") 1956 .on("click.priceFilterApply", ".price-filter-apply", function (e) { 1957 e.preventDefault(); 1958 var $table = $(this).closest(".price-filter-table"); 1959 if (!$table.length) return; 1960 var bounds = getPriceFilterBounds($table); 1961 var minVal = parsePriceValue($table.find(".price-filter-input-min").val()); 1962 var maxVal = parsePriceValue($table.find(".price-filter-input-max").val()); 1963 if (!isFinite(minVal) && !isFinite(maxVal)) { 1964 sendPriceFilterRequest({ reset: true }); 1965 } else { 1966 var range = normalizePriceRange(minVal, maxVal, bounds); 1967 sendPriceFilterRequest(range); 1968 } 1969 }); 1970 1971 $(document) 1972 .off("click.priceFilterReset") 1973 .on("click.priceFilterReset", ".price-filter-reset", function (e) { 1974 e.preventDefault(); 1975 var $table = $(this).closest(".price-filter-table"); 1976 if ($table.length) { 1977 var bounds = getPriceFilterBounds($table); 1978 syncPriceFilterFields($table, bounds); 1979 } 1980 sendPriceFilterRequest({ reset: true }); 1981 }); 1982 } 1983 1984 function setMobileFiltersState(shouldOpen) { 1985 var $container = $("#mobile-filters-container"); 1986 if (!$container.length) return; 1987 if (typeof shouldOpen === "undefined") { 1988 shouldOpen = $container.hasClass("mobile-filters-hide"); 1989 } 1990 $container.toggleClass("mobile-filters-hide", !shouldOpen); 1991 $("body").css("overflow", shouldOpen ? "hidden" : "auto"); 1992 } 1993 1994 function bindMenuToggles() { 1995 $(document) 1996 .off("click.mobileMainMenu") 1997 .on("click.mobileMainMenu", ".mobile-mainmenu", function (e) { 1998 e.preventDefault(); 1999 var id = $(this).attr("id_code"); 2000 if (!id) return; 2001 $("#subcat_" + id).toggleClass("mobile-submenu-open"); 2002 $(this).find(".menu-sub-icon").toggleClass("fa-plus fa-minus"); 2003 }); 2004 2005 $(document) 2006 .off("click.desktopMainMenu") 2007 .on("click.desktopMainMenu", ".desktop-mainmenu", function (e) { 2008 e.preventDefault(); 2009 var id = $(this).attr("id_code"); 2010 if (!id) return; 2011 $("#desktop_subcat_" + id).toggleClass("desktop-submenu-open"); 2012 $(this).find(".menu-sub-icon").toggleClass("fa-plus fa-minus"); 2013 }); 2014 2015 $(".mobile_maincat.active").each(function () { 2016 var id = $(this).attr("id_code"); 2017 if (id) { 2018 $("#plus_" + id).trigger("click"); 2019 } 2020 }); 2021 2022 $(".desktop_maincat.active").each(function () { 2023 var id = $(this).attr("id_code"); 2024 if (id) { 2025 $("#desktop_plus_" + id).trigger("click"); 2026 } 2027 }); 2028 2029 $(document) 2030 .off("click.hamburguer") 2031 .on("click.hamburguer", "#hamburguer", function (e) { 2032 e.preventDefault(); 2033 toggleLeftMenu(); 2034 }); 2035 2036 $(document) 2037 .off("click.mobileFiltersToggle") 2038 .on("click.mobileFiltersToggle", ".mobile-filters-close, #mobile-filters-btn", function (e) { 2039 e.preventDefault(); 2040 if (typeof updatefilters === "function") updatefilters(); 2041 var $container = $("#mobile-filters-container"); 2042 var openNext = $container.hasClass("mobile-filters-hide"); 2043 setMobileFiltersState(openNext); 2044 }); 2045 2046 $(document) 2047 .off("click.mobileFiltersReset") 2048 .on("click.mobileFiltersReset", ".mobile-filters-reset", function (e) { 2049 if ($(this).is(":disabled")) return; 2050 e.preventDefault(); 2051 pendingFiltersReset = true; 2052 updateMobileFiltersReset(false); 2053 updateDesktopFiltersReset(false); 2054 Cat.page = "1"; sv("#nowpage", "1"); 2055 scheduleCatalogScrollTop(); 2056 syncThenRender({ resetAll: true }); 2057 }); 2058 2059 $(document) 2060 .off("click.mobileFiltersApply") 2061 .on("click.mobileFiltersApply", ".mobile-filters-apply", function (e) { 2062 e.preventDefault(); 2063 setMobileFiltersState(false); 2064 }); 2065 2066 } 2067 2068 function bindScrollHelpers() { 2069 var $toTop = $(".cd-top"); 2070 $(window).off("scroll.ux").on("scroll.ux", function () { 2071 if ($(this).scrollTop() > 300) $toTop.addClass("cd-is-visible"); else $toTop.removeClass("cd-is-visible cd-fade-out"); 2072 if ($(this).scrollTop() > 1200) $toTop.addClass("cd-fade-out"); 2073 if ($.fn.lazy) $(".lazy").lazy(); 2074 }); 2075 2076 $toTop.off("click.ux").on("click.ux", function (e) { 2077 e.preventDefault(); 2078 $("body,html").animate({ scrollTop: 0 }, 700); 2079 }); 2080 } 2081 2082 function bindProductSizeSelector() { 2083 $(document) 2084 .off("click.prodSize") 2085 .on("click.prodSize", ".sizebox", function (e) { 2086 e.preventDefault(); 2087 2088 var $btn = $(this); 2089 var id_code = $btn.attr("rel") || $btn.data("id") || $btn.data("rel"); 2090 var stock = parseInt($btn.attr("stock") || $btn.data("stock") || "0", 10); 2091 var n_cor = $btn.attr("n_cor") || $btn.data("n_cor") || ""; 2092 var n_tamanho = $btn.attr("n_tamanho") || $btn.data("n_tamanho") || ""; 2093 var estado = $btn.attr("estado") || $btn.data("estado") || ""; // "on" ou "" 2094 2095 $("#n_cor").val(n_cor); 2096 $("#n_tamanho").val(n_tamanho); 2097 $("#n_tamanho_box").val(n_tamanho); 2098 $("#fake_size").val(n_tamanho); 2099 $("#stock").val(stock); 2100 2101 $(".sizebox").removeClass("size_btn_active"); 2102 2103 if (estado === "on" && stock !== 0 && stock !== -1) { 2104 $("#btn-shopcart-ficha").removeAttr("disabled") 2105 .removeClass("btn-shopcart-ficha-disabled"); 2106 $btn.addClass("size_btn_active"); 2107 2108 var opts = ""; 2109 for (var i = 1; i <= stock; i++) { 2110 opts += '<option value="' + i + '">' + i + "</option>"; 2111 } 2112 $("#stock_select").empty().append(opts); 2113 2114 $("#nosizeavailable-a").removeClass("geralOrange"); 2115 $("#nosizeavailable-i").removeClass("geralOrangeIcon"); 2116 } else { 2117 toastr.error(gl("ALERTA_TAMANHO_NAO_DISPONIVEL")); 2118 $("#btn-shopcart-ficha").addClass("btn-shopcart-ficha-disabled") 2119 .attr("disabled", "true"); 2120 $(".sizebox").removeClass("size_btn_active"); 2121 $("#nosizeavailable-a").addClass("geralOrange"); 2122 $("#nosizeavailable-i").addClass("geralOrangeIcon"); 2123 } 2124 }); 2125 } 2126 2127function bindProductAddToCart() { 2128 $(document) 2129 .off("click.addFicha") 2130 .on("click.addFicha", "#btn-shopcart-ficha", function (e) { 2131 e.preventDefault(); 2132 2133 var $btn = $(this); 2134 var $spinner = $("#btn-shopcart-ficha-loading"); 2135 2136 var id_code = $("#id_code").val() || $btn.data("id_code") || $btn.attr("id_code"); 2137 var n_cor = $("#n_cor").val() || ""; 2138 var n_tamanho = $("#n_tamanho").val() || ""; 2139 var qty = $("#stock_select").val() || 1; 2140 var sizeBox = $("#n_tamanho_box").val() || ""; 2141 2142 if (!id_code) return; 2143 2144 // validação de tamanho 2145 var $tabelaTamanhos = $("#tabela_tamanhos"); 2146 if ($tabelaTamanhos.length && (!sizeBox || sizeBox === "0")) { 2147 if (typeof toastr !== "undefined") { 2148 toastr.warning(gl("ALERT_NOPRODS_CESTO") || "Selecione um tamanho antes de continuar."); 2149 } 2150 $tabelaTamanhos.addClass("tabela_tamanhos_alert"); 2151 setTimeout(function () { 2152 $tabelaTamanhos.removeClass("tabela_tamanhos_alert"); 2153 }, 9000); 2154 return; 2155 } 2156 2157 // ------------------------------------------------------------------ 2158 // OFERTAS: ler dos inputs hidden (novo) ou reconstruir como fallback 2159 // ------------------------------------------------------------------ 2160 var selectedOfferId = $("#selected_offer_id").val() || null; 2161 var selectedOfferIds = $("#selected_offer_ids").val() || ""; 2162 2163 if (!selectedOfferIds) { 2164 // fallback: construir a string id:qty|id2:qty2 a partir dos checkboxes 2165 var parts = []; 2166 $(".offering_items_container .cCheck:checked").each(function () { 2167 var $item = $(this).closest(".offering_item"); 2168 if (!$item.length) return; 2169 2170 var oid = $item.data("id") || $item.attr("data-id"); 2171 if (!oid) return; 2172 2173 var $sel = $item.find(".offer_qty_select"); 2174 var q = parseInt($sel.val(), 10); 2175 if (isNaN(q) || q < 1) q = 1; 2176 2177 parts.push(oid + ":" + q); 2178 if (!selectedOfferId) selectedOfferId = oid; 2179 }); 2180 2181 if (parts.length) { 2182 selectedOfferIds = parts.join("|"); 2183 } 2184 } 2185 2186 // ------------------------------------------------------------------ 2187 // Preparar payload 2188 // ------------------------------------------------------------------ 2189 $btn.prop("disabled", true).hide(); 2190 $spinner.show(); 2191 2192 var payload = { 2193 ajax: "shopcart_add", 2194 id_code: id_code, 2195 stock: qty, 2196 n_cor: n_cor, 2197 n_tamanho: n_tamanho, 2198 n_tamanho_box: sizeBox, 2199 fake_size: $("#fake_size").val() || "", 2200 qty: qty 2201 }; 2202 2203 if (selectedOfferId) { 2204 payload.selected_offer_id = selectedOfferId; // legacy 2205 } 2206 if (selectedOfferIds) { 2207 payload.selected_offer_ids = selectedOfferIds; // novo formato id:qty|id2:qty2 2208 } 2209 2210 $.ajax({ 2211 type: "POST", 2212 url: REQUEST_ENDPOINT, 2213 async: true, 2214 cache: false, 2215 data: payload 2216 }) 2217 .done(function (resp) { 2218 var data = {}; 2219 try { 2220 data = (typeof resp === "object") ? resp : JSON.parse(resp); 2221 } catch (err) { 2222 data = {}; 2223 } 2224 2225 if (data.status === 1) { 2226 // GA4 / DataLayer 2227 if (data.item) { 2228 window.dataLayer = window.dataLayer || []; 2229 window.dataLayer.push({ 2230 event: "add_to_cart", 2231 ecommerce: { 2232 currency: data.item.currency || "EUR", 2233 value: data.item.price, 2234 items: [data.item] 2235 } 2236 }); 2237 } 2238 2239 if (typeof window.check_shopcart === "function") window.check_shopcart(); 2240 if (typeof t === "function") t(id_code); 2241 2242 $(".fichaGotoCart").fadeIn(600); 2243 $(".shopcart-selector-right, .shopcart-selector-left").addClass("flash"); 2244 setTimeout(function () { 2245 $(".shopcart-selector-right, .shopcart-selector-left").removeClass("flash"); 2246 }, 3000); 2247 2248 /* t1592: sem toast «Produto adicionado ao cesto!» â badge/flash do cesto bastam */ 2249 2250 } else if (data.error === "maxsamples" || data.status === "maxsamples") { 2251 // compat: aceita error ou antigo status 2252 if (typeof Swal !== "undefined") { 2253 Swal.fire({ 2254 title: gl("swal_attention") || "Atenção", 2255 text: gl("SAMPLES_MAXEDOUT") || "Atingiu o limite de amostras.", 2256 icon: "warning", 2257 confirmButtonColor: "#ABD559", 2258 confirmButtonText: gl("swal_btnok") || "OK" 2259 }); 2260 } else if (typeof toastr !== "undefined") { 2261 toastr.warning(gl("SAMPLES_MAXEDOUT") || "Atingiu o limite de amostras."); 2262 } 2263 2264 } else { 2265 var message = data.message || gl("ALERT_NOPRODS_CESTO") || "Não foi possÃvel adicionar ao carrinho."; 2266 if (typeof Swal !== "undefined") { 2267 Swal.fire(gl("swal_attention") || "Atenção", message, "warning"); 2268 } else if (typeof toastr !== "undefined") { 2269 toastr.error(message); 2270 } 2271 } 2272 }) 2273 .fail(function () { 2274 if (typeof toastr !== "undefined") { 2275 toastr.error(gl("ALERT_NOPRODS_CESTO") || "Não foi possÃvel adicionar ao carrinho."); 2276 } 2277 }) 2278 .always(function () { 2279 $spinner.hide(); 2280 $btn.show().prop("disabled", false); 2281 }); 2282 }); 2283} 2284 2285 2286 2287 function bindProductTabsNavigation() { 2288 $(document) 2289 .off("click.navT") 2290 .on("click.navT", ".navigationT, .tabsT a", function (e) { 2291 e.preventDefault(); 2292 var $link = $(this); 2293 var targetId = ($link.attr("href") || $link.data("target") || $link.attr("id") || "").replace(/^#/, ""); 2294 if (!targetId) return; 2295 2296 $(".tabsT a, .navigationT").removeClass("selected"); 2297 $link.addClass("selected"); 2298 2299 $(".tabInfoT .tabT").hide(); 2300 $(".tabInfoT ." + targetId).fadeIn("fast"); 2301 }); 2302 } 2303 2304 // ===== alterar quantidade no carrinho =====
2305 function bindCartQuantityControls() { 2306 $(document).off("change.cartQty").on("change.cartQty", "#stock_shopcart, .stock_shopcart_element", function () { 2307 var $select = $(this); 2308 2309 var id_code = $select.attr("id_code") || $select.data("id_code"); 2310 var element_id_code = $select.attr("element_id_code") || $select.data("element_id_code"); 2311 var current_qtd = $select.attr("current_qtd") || 1; 2312 var new_qtd = $select.val(); 2313 2314 $.ajax({ 2315 type: "POST", 2316 url: REQUEST_ENDPOINT, 2317 data: { 2318 ajax: "shopcart_change_qtd", 2319 id_code: id_code, 2320 element_id_code: element_id_code, 2321 current_qtd: current_qtd, 2322 new_qtd: new_qtd 2323 } 2324 }).done(function () { 2325 var reloadType = $("#delivery_methods").is(":visible") ? "shopcart_remove" : "html"; 2326 if (typeof generate_shopcart === "function") generate_shopcart(reloadType); 2327 if (typeof check_shopcart === "function") check_shopcart(); 2328 if ($(".checkout-modern").length && typeof window.refreshCheckoutProductsCard === "function") { 2329 window.refreshCheckoutProductsCard({ refreshLists: false }); 2330 } 2331 if (typeof sendCheckoutEvent === "function") { 2332 sendCheckoutEvent(2, "checkout_progress"); 2333 } 2334 }).fail(function () { 2335 if (typeof toastr !== "undefined") { 2336 toastr.error(gl("ALERT_NOPRODS_CESTO") || "Não foi possÃvel atualizar a quantidade."); 2337 } else { 2338 console.error("shopcart_change_qtd failed"); 2339 } 2340 }); 2341 }); 2342 } 2343 2344 function bindDiscountHandlers() { 2345 $(document) 2346 .off("click.applyDiscount") 2347 .on("click.applyDiscount", "#apply_discount", function (e) { 2348 e.preventDefault(); 2349 2350 var $button = $(this); 2351 var $scope = $button.closest(".checkout-modern__promo-form, #shopcart_discount_html"); 2352 var $input = $scope.find("#discount_code").first(); 2353 if (!$input.length) $input = $(".checkout-modern__cart-aside #discount_code").first(); 2354 if (!$input.length) $input = $("#discount_code").last(); 2355 2356 if ($button.hasClass("is-removing-discount") || $.trim($button.val()).toUpperCase() === "REMOVER") { 2357 $("#discount_code").val(""); 2358 $button.removeData("appliedCode").val("APLICAR").removeClass("is-removing-discount"); 2359 if (typeof generate_shopcart === "function") generate_shopcart("html", ""); 2360 if (typeof generate_shopcart_discounts === "function") generate_shopcart_discounts(""); 2361 if (typeof populate_shopcart === "function") populate_shopcart(); 2362 return; 2363 } 2364 2365 var code = $.trim($input.val() || ""); 2366 if (!code) { 2367 if (typeof Swal !== "undefined") { 2368 Swal.fire("Atenção", "Por favor introduza um código de desconto.", "info"); 2369 } else if (typeof toastr !== "undefined") { 2370 toastr.warning("Por favor introduza um código de desconto."); 2371 } 2372 return; 2373 } 2374 2375 $.ajax({ 2376 type: "POST", 2377 url: REQUEST_ENDPOINT, 2378 cache: false, 2379 data: { ajax: "validate_discount_code", discount_code: code } 2380 }).done(function (resp) { 2381 var clean = (resp || "").toString().trim(); 2382 var isValid = !isNaN(clean) && parseFloat(clean) > 0; 2383 2384 if (isValid) { 2385 $("#discount_code").val(code); 2386 $button.data("appliedCode", code).val("REMOVER").addClass("is-removing-discount"); 2387 if (typeof Swal !== "undefined") { 2388 Swal.fire("Desconto aplicado", "O seu código foi aplicado com sucesso.", "success"); 2389 } else if (typeof toastr !== "undefined") { 2390 toastr.success("Desconto aplicado com sucesso."); 2391 } 2392 2393 if (typeof generate_shopcart === "function") generate_shopcart("html", code); 2394 if (typeof generate_shopcart_discounts === "function") generate_shopcart_discounts(code); 2395 if (typeof populate_shopcart === "function") populate_shopcart(); 2396 } else { 2397 if (typeof Swal !== "undefined") { 2398 Swal.fire("Desconto inválido", "O código que introduziu não é válido.", "error"); 2399 } else if (typeof toastr !== "undefined") { 2400 toastr.error("Código de desconto inválido."); 2401 } 2402 2403 if (typeof generate_shopcart_discounts === "function") generate_shopcart_discounts(""); 2404 $("#discount_code").val(""); 2405 $button.removeData("appliedCode").val("APLICAR").removeClass("is-removing-discount"); 2406 } 2407 }).fail(function () { 2408 toastr.error("Não foi possÃvel validar o código de desconto."); 2409 }); 2410 }); 2411 2412 $(document) 2413 .off("change.discountCodeToggle") 2414 .on("change.discountCodeToggle", "#discount_code_checkbox", function () { 2415 if (this.checked) { 2416 var code = $("#discount_code").val(); 2417 if (code) $("#apply_discount").trigger("click"); 2418 return; 2419 } 2420 2421 $.post(REQUEST_ENDPOINT, { ajax: "validate_discount_code", discount_code: "" }, function () { 2422 if (typeof generate_shopcart === "function") generate_shopcart("html"); 2423 if (typeof generate_shopcart_discounts === "function") generate_shopcart_discounts(""); 2424 2425 if (typeof Swal !== "undefined") { 2426 Swal.fire({ 2427 toast: true, 2428 position: "top-end", 2429 icon: "info", 2430 title: "Desconto removido",
2431 showConfirmButton: false, 2432 timer: 2000, 2433 timerProgressBar: true 2434 }); 2435 } else if (typeof toastr !== "undefined") { 2436 toastr.info("Desconto removido."); 2437 } 2438 }); 2439 }); 2440 } 2441 2442// ===== FINALIZAR ENCOMENDA ===== 2443 function bindCheckoutFlowEvents() { 2444 $(document) 2445 .off("click.shopcartCheckout") 2446 .on("click.shopcartCheckout", "#shopcart_checkout_button", function (e) { 2447 e.preventDefault(); 2448 2449 var hasItems = 1; 2450 if ($("#check_amostras").length) { 2451 hasItems = parseInt($("#check_amostras").val(), 10) || 0; 2452 } 2453 if (hasItems < 1) { 2454 var samplesOnly = $("#check_amostras").attr("data-samples-only") === "1"; 2455 var emptyMsg = samplesOnly 2456 ? (gl("CV2_SAMPLES_ONLY_CHECKOUT") || "Não é possÃvel finalizar uma encomenda só com amostras. Adicione pelo menos um produto da loja.") 2457 : (gl("COMPRAS_A_ZERO") || "O seu carrinho está vazio."); 2458 if (typeof Swal !== "undefined") { 2459 Swal.fire({ 2460 title: "", 2461 text: emptyMsg, 2462 icon: "info", 2463 confirmButtonText: gl("swal_btnok") || "OK" 2464 }); 2465 } else if (typeof toastr !== "undefined") { 2466 toastr.warning(emptyMsg); 2467 } else { 2468 alert(emptyMsg); 2469 } 2470 return; 2471 } 2472 2473 try { 2474 window.dataLayer = window.dataLayer || []; 2475 window.dataLayer.push({ 2476 event: "gtm_begin_checkout", 2477 event_category: "ecommerce", 2478 event_label: "Finalizar encomenda" 2479 }); 2480 } catch (err) {} 2481 2482 if (typeof toastr !== "undefined") toastr.clear(); 2483 $("#shopcart_option_buttons").stop(true, true).slideUp(150, function () { 2484 $("#shopcart_option_buttons").hide(); 2485 }); 2486 $("#footer_container").stop(true, true).fadeOut(150); 2487 2488 if (typeof check_shopcart === "function") check_shopcart("init"); 2489 if (typeof populate_shopcart === "function") populate_shopcart(); 2490 if (typeof shopcart_specialservices === "function") shopcart_specialservices(); 2491 if (typeof generate_shopcart === "function") generate_shopcart("html"); 2492 2493 $.post(REQUEST_ENDPOINT, { ajax: "generate_shopcart", type: "html" }, function (html) { 2494 var $desktopPanel = $("#shopcart-modal-container"); 2495 var $mobilePanel = $("#mini-cart"); 2496 2497 if ($desktopPanel.length) { 2498 if ($("#shopcart-modal").length) $("#shopcart-modal").html(html); 2499 else $desktopPanel.html(html); 2500 } 2501 if ($mobilePanel.length) $mobilePanel.html(html); 2502 }); 2503 2504 $("#encomenda").fadeIn(300); 2505 var $enc = $("#encomenda"); 2506 if ($enc.length) { 2507 $("html, body").animate({ scrollTop: $enc.offset().top - 80 }, 400); 2508 } 2509 }); 2510 2511 $(document) 2512 .off("click.shopcartBack") 2513 .on("click.shopcartBack", "#shopcart_shopping_button", function (e) { 2514 e.preventDefault(); 2515 2516 $.ajax({ 2517 type: "POST", 2518 url: REQUEST_ENDPOINT, 2519 cache: false, 2520 data: { ajax: "getReferralByProduct", idcode: $("#latest_idcode").val() } 2521 }).done(function (url) { 2522 if (typeof url === "string" && url.trim()) { 2523 window.location.href = url.trim(); 2524 } 2525 }); 2526 }); 2527 2528 $(document) 2529 .off("click.shopcartDelivery") 2530 .on("click.shopcartDelivery", ".shopcart_delivery_button", function (e) { 2531 e.preventDefault(); 2532 $("#special_service").val(""); 2533 if (typeof shopcart_specialservices === "function") shopcart_specialservices(); 2534 clickDeliveryMethod($(this).attr("id")); 2535 check_shopcart(); 2536 }); 2537 2538 $(document) 2539 .off("click.shopcartPaytype") 2540 .on("click.shopcartPaytype", ".shopcart_paytype_button", function (e) { 2541 e.preventDefault(); 2542 clickPayMethod($(this).attr("id")); 2543 check_shopcart(); 2544 }); 2545 2546 $(document) 2547 .off("change.shopcartCountry") 2548 .on("change.shopcartCountry", "#country, #delivery_country", function () { 2549 $("#special_service").val(""); 2550 if (typeof shopcart_specialservices === "function") shopcart_specialservices(); 2551 // CV2: sincronizar o pais guest com a sidebar ANTES dos populates 2552 if (this.id === "country" && window.cv2SetCountry && !($("#uid").val() > 0)) window.cv2SetCountry($(this).val()); 2553 check_shopcart(); 2554 // CV2: recalcular portes/totais (check_shopcart so atualiza o header) 2555 if (typeof populate_shopcart === "function") populate_shopcart(); 2556 }); 2557 } 2558 2559 function bindGiftAndInfoToggles() { 2560 $(document) 2561 .off("click.scInfoToggle") 2562 .on("click.scInfoToggle", "#sc_information_text", function (e) { 2563 e.preventDefault(); 2564 $("#sc_information_text_container").toggleClass("fold", 500); 2565 }); 2566 2567 $(document) 2568 .off("change.shopcartGift") 2569 .on("change.shopcartGift", "#send_as_gift", function () { 2570 populate_shopcart("send_as_gift"); 2571 }); 2572 } 2573 2574 function bindLiveSearchEvents() { 2575 $(document) 2576 .off("scroll.livesearchResults") 2577 .on("scroll.livesearchResults", ".livesearch-results", function () { 2578 if ($("#livesearch").css("display") === "block" && $("#q").is(":focus")) { 2579 $("#q").blur(); 2580 } 2581 if ($(this).scrollTop() === 0) { 2582 $("#q").focus(); 2583 } 2584 }); 2585 2586 $(document) 2587 .off("keyup.livesearch keypress.livesearch click.livesearch", "#q.livesearch-input") 2588 .on("keyup.livesearch keypress.livesearch click.livesearch", "#q.livesearch-input", function (evt) { 2589 if (window.CV2_SEARCH_OVERLAY) return; 2590 var $input = $(this); 2591 $("#livesearch").scrollTop(0); 2592 var query = $input.val(); 2593 var len = query.length; 2594 var key = evt.keyCode || evt.which; 2595 2596 if (len > 3) { 2597 if (key === 13) { evt.preventDefault(); return false; } 2598 if (key === 8) toggleLiveSearch("show"); 2599 if (key !== 27 && key !== 1 && key !== 8 && key !== 32) { 2600 $.ajax({ 2601 type: "POST", 2602 cache: true, 2603 url: "/livesearch.php", 2604 data: { q: query }, 2605 timeout: 2000 2606 }).done(function (html) { 2607 toggleLiveSearch("show"); 2608 $("#livesearch").html(html); 2609 }); 2610 } 2611 } else { 2612 toggleLiveSearch("reset"); 2613 } 2614 2615 $input.focus(); 2616 }); 2617 2618 $(document) 2619 .off("click.livesearchProduct") 2620 .on("click.livesearchProduct", ".livesearch-product-href", function (e) { 2621 var href = $(this).attr("href"); 2622 if (href) { 2623 e.preventDefault(); 2624 window.location.assign(href); 2625 } 2626 }); 2627 2628 $(document) 2629 .off("click.livesearchReset") 2630 .on("click.livesearchReset", "#livesearch-reset, .livesearch-noresults-container", function () { 2631 $("html").css({ overflow: "auto" }); 2632 toggleLiveSearch("hide"); 2633 $("body").animate({ scrollTop: 0 }, 200); 2634 }); 2635 2636 $(document) 2637 .off("click.livesearchGoUp") 2638 .on("click.livesearchGoUp", "#goUp", function (e) { 2639 e.preventDefault(); 2640 $("body").animate({ scrollTop: 0 }, 200); 2641 $("#livesearch").animate({ scrollTop: 0 }, 200); 2642 }); 2643 2644 $(document) 2645 .off("click.livesearchFavAdd") 2646 .on("click.livesearchFavAdd", ".livesearch_favorite_add", function (e) { 2647 e.preventDefault(); 2648 var id = $(this).attr("id_code") || $(this).data("id_code"); 2649 if (!id) return; 2650 2651 var $icon = $(this); 2652 $.post(REQUEST_ENDPOINT, { ajax: "shopcart_favorites_add", id_code: id }, () => { 2653 $icon 2654 .removeClass("livesearch_favorite_add fa-heart-o far") 2655 .addClass("livesearch_favorite_remove fa fa-heart"); 2656 toastr.info(gl("LIVES_ADD_FAVORITE_INFO") || "Adicionado aos favoritos"); 2657 }); 2658 }); 2659 2660 $(document) 2661 .off("click.livesearchFavRemove") 2662 .on("click.livesearchFavRemove", ".livesearch_favorite_remove", function (e) { 2663 e.preventDefault(); 2664 var id = $(this).attr("id_code") || $(this).data("id_code"); 2665 if (!id) return; 2666 2667 var $icon = $(this); 2668 $.post(REQUEST_ENDPOINT, { ajax: "shopcart_favorites_remove", id_code: id }, () => { 2669 $icon 2670 .removeClass("livesearch_favorite_remove fa") 2671 .addClass("livesearch_favorite_add far fa-heart-o"); 2672 toastr.warning(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos"); 2673 }); 2674 }); 2675 } 2676 2677 function bindDeliveryApiLanguageSwitch() { 2678 $(document) 2679 .off("click.cttLangPt") 2680 .on("click.cttLangPt", "#ctt_lang_pt", function () { 2681 // sem preventDefault: impedia o radio de ficar checked (estado do pais nunca mudava) 2682 this.checked = true; 2683 callAPIresults("pt"); 2684 }); 2685 2686 $(document) 2687 .off("click.cttLangEs") 2688 .on("click.cttLangEs", "#ctt_lang_es", function () { 2689 this.checked = true; 2690 callAPIresults("es"); 2691 }); 2692 } 2693 2694 function shopcartDrawerEscapeHtml(value) { 2695 return String(value == null ? "" : value) 2696 .replace(/&/g, "&") 2697 .replace(/</g, "<") 2698 .replace(/>/g, ">") 2699 .replace(/"/g, """) 2700 .replace(/\x27/g, "'"); 2701 } 2702 2703 function closeShopcartRemoveModal() { 2704 $(".cupido-cart-remove-scrim").remove(); 2705 $(document).off("keyup.shopcartRemoveModal"); 2706 $("body").removeClass("cupido-cart-remove-modal-open"); 2707 } 2708 2709 function shopcartSavedForLaterAvailable() { 2710 var uid = parseInt($("#session_uid").val() || "0", 10); 2711 return uid > 0; 2712 } 2713 2714 function getShopcartRemoveModalProduct($button) { 2715 var $item = $button.closest(".shopcart-drawer__item"); 2716 var $row = $button.closest("#shopcart_html .shop_cart_table tr"); 2717 var $title = $item.length ? $item.find(".shopcart-drawer__item-title").first().clone() : $row.children("td").eq(2).clone();
2718 var $name = $title.find("a").first(); 2719 var imgSrc = $item.length ? ($item.find(".shopcart-drawer__thumb img").first().attr("src") || "") : ($row.find("img").first().attr("src") || ""); 2720 var href = $name.attr("href") || "#"; 2721 var name = $.trim(($name.text() || $title.text() || "").replace(/\s+/g, " ")); 2722 var quantity = ($item.length ? $item.find(".shopcart-drawer__qty-select").first().val() : $row.find("select").first().val()) || "1"; 2723 var price = $.trim(($item.length ? $item.find(".shopcart-drawer__price-total").first().text() : $row.find(".checkout-cart-line-total").first().text()) || ""); 2724 2725 return { 2726 image: imgSrc, 2727 href: href, 2728 name: name, 2729 meta: quantity + " " + (gl("SHOPCART_REMOVE_MODAL_UNIT") || "un.") + (price ? " · " + price : "") 2730 }; 2731 } 2732 2733 function openShopcartRemoveModal($button, key, eKey, removeItem) { 2734 var product = getShopcartRemoveModalProduct($button); 2735 var title = gl("SHOPCART_REMOVE_MODAL_TITLE") || "Remover do cesto?"; 2736 var message = gl("SHOPCART_SAVE_FOR_LATER_MESSAGE") || "Guarde-o nos favoritos para o encontrar mais tarde."; 2737 var saveEnabled = shopcartSavedForLaterAvailable(); 2738 var saveLabel = gl("SHOPCART_SAVE_FOR_LATER") || "Guardar para depois"; 2739 var saveMessageHtml = saveEnabled ? "<p class=\"pv-modal__text pv-modal__text--cart-remove\">" + shopcartDrawerEscapeHtml(message) + "</p>" : ""; 2740 var saveActionHtml = saveEnabled ? "<button type=\"button\" class=\"cup-cart-remove-btn cup-cart-remove-btn--link\" data-cart-remove-save>" + shopcartDrawerEscapeHtml(saveLabel) + "</button>" : "<span class=\"cup-cart-remove-spacer\"></span>"; 2741 var cancelLabel = gl("swal_btncancel") || "Cancelar"; 2742 var removeLabel = gl("SHOPCART_REMOVE") || gl("swal_btnyes") || "Remover"; 2743 var productHtml = [ 2744 "<a class=\"pv-modal-product\" href=\"", shopcartDrawerEscapeHtml(product.href), "\">", 2745 "<span class=\"pv-modal-product__image\">", 2746 product.image ? "<img src=\"" + shopcartDrawerEscapeHtml(product.image) + "\" alt=\"" + shopcartDrawerEscapeHtml(product.name) + "\">" : "", 2747 "</span>", 2748 "<span class=\"pv-modal-product__content\">", 2749 "<strong class=\"pv-modal-product__name\">", shopcartDrawerEscapeHtml(product.name), "</strong>", 2750 "<span class=\"pv-modal-product__meta\">", shopcartDrawerEscapeHtml(product.meta), "</span>", 2751 "</span>", 2752 "</a>" 2753 ].join(""); 2754 2755 closeShopcartRemoveModal(); 2756 $("body").addClass("cupido-cart-remove-modal-open").append([ 2757 "<div class=\"pv-scrim cupido-cart-remove-scrim\" role=\"presentation\">", 2758 "<div class=\"pv-modal pv-modal--cart-remove\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"cupido-cart-remove-title\">", 2759 "<button type=\"button\" class=\"pv-modal__close\" data-cart-remove-close aria-label=\"Fechar\">×</button>", 2760 "<div class=\"pv-modal__head\"><h3 id=\"cupido-cart-remove-title\" class=\"pv-modal__title\">", shopcartDrawerEscapeHtml(title), "</h3></div>", 2761 "<div class=\"pv-modal__body\">", productHtml, saveMessageHtml, "</div>", 2762 "<div class=\"pv-modal__foot pv-modal__foot--cart-remove\">", 2763 saveActionHtml, 2764 "<span class=\"pv-modal__foot-actions\">", 2765 "<button type=\"button\" class=\"cup-cart-remove-btn cup-cart-remove-btn--ghost\" data-cart-remove-close>", shopcartDrawerEscapeHtml(cancelLabel), "</button>", 2766 "<button type=\"button\" class=\"cup-cart-remove-btn cup-cart-remove-btn--danger\" data-cart-remove-confirm>", shopcartDrawerEscapeHtml(removeLabel), "</button>", 2767 "</span>", 2768 "</div>", 2769 "</div>", 2770 "</div>" 2771 ].join("")); 2772 2773 $(document).on("keyup.shopcartRemoveModal", function (event) { 2774 if (event.key === "Escape" || event.keyCode === 27) closeShopcartRemoveModal(); 2775 }); 2776 2777 $(".cupido-cart-remove-scrim") 2778 .off("click.shopcartRemoveModal") 2779 .on("click.shopcartRemoveModal", function (event) { 2780 if ($(event.target).is(".cupido-cart-remove-scrim, [data-cart-remove-close]")) { 2781 event.preventDefault(); 2782 closeShopcartRemoveModal(); 2783 } 2784 }) 2785 .on("click.shopcartRemoveModal", "[data-cart-remove-confirm]", function (event) { 2786 event.preventDefault(); 2787 closeShopcartRemoveModal(); 2788 removeItem(); 2789 }) 2790 .on("click.shopcartRemoveModal", "[data-cart-remove-save]", function (event) { 2791 event.preventDefault(); 2792 var finish = function () { 2793 closeShopcartRemoveModal(); 2794 removeItem(); 2795 }; 2796 2797 if (key) { 2798 $.post(REQUEST_ENDPOINT, { ajax: "shopcart_saved_for_later_add", id_code: key, id_produto_elemento: eKey || 0 }, function (response) { 2799 var data = {}; 2800 try { data = (typeof response === "object") ? response : JSON.parse(response); } catch (err) { data = {}; } 2801 if (data.ok) finish(); 2802 }); 2803 } 2804 }); 2805 } 2806 2807 function bindCartRemovalEvents() { 2808 $(document) 2809 .off("click.shopcartRemove") 2810 .on("click.shopcartRemove", ".shopcart_remove", function (e) { 2811 e.preventDefault(); 2812 e.stopPropagation(); 2813 var key = $(this).attr("key"); 2814 var eKey = $(this).attr("e_key"); 2815 if (!key && !eKey) return; 2816 2817 var removeItem = function () { 2818 var drawerItems = $(".shopcart-drawer__item").length; 2819 var tableRemoves = $("#shopcart_html .shopcart_remove").length; 2820 if ((drawerItems > 0 && drawerItems <= 1) || (tableRemoves > 0 && tableRemoves <= 1)) { 2821 window.__cv2UserEmptiedCart = true; 2822 } 2823 window.__cv2CartRemoving = true; 2824 if (typeof window.setShopcartDrawerLoading === "function") window.setShopcartDrawerLoading(true); 2825 $.ajax({ 2826 type: "POST", 2827 url: REQUEST_ENDPOINT, 2828 cache: false, 2829 data: { ajax: "shopcart_remove", key: key, e_key: eKey } 2830 }).done(function (response) { 2831 var res = {}; 2832 if (typeof response === "string") { 2833 try { res = JSON.parse(response); } catch (err) { res = {}; } 2834 } else if (typeof response === "object") { 2835 res = response || {}; 2836 } 2837 2838 if (res.status === 1 && res.item) { 2839 window.dataLayer = window.dataLayer || []; 2840 window.dataLayer.push({ 2841 event: "remove_from_cart", 2842 currency: "EUR", 2843 items: [res.item] 2844 }); 2845 } 2846 2847 if ($("body").hasClass("shopcart-drawer-open") && typeof window.openShopcartDrawer === "function") { 2848 window.openShopcartDrawer(); 2849 } 2850 if ($("#shopcart_html").length) { 2851 refreshCheckoutAfterCartChange(); 2852 } else if (!$("body").hasClass("shopcart-drawer-open")) { 2853 window.location.reload(); 2854 } 2855 }).always(function () { 2856 window.__cv2CartRemoving = false;
2857 if (typeof window.setShopcartDrawerLoading === "function") window.setShopcartDrawerLoading(false); 2858 }); 2859 }; 2860 2861 if ($(this).closest(".shopcart-drawer__item").length || $(this).closest("#shopcart_html .shop_cart_table").length) { 2862 openShopcartRemoveModal($(this), key, eKey, removeItem); 2863 return; 2864 } 2865 2866 if (typeof swal === "function") { 2867 swal({ 2868 title: gl("swal_attention") || "Atenção", 2869 text: gl("swal_action_will_delete_item") || "Deseja remover este item?", 2870 type: "warning", 2871 confirmButtonColor: "#ABD559", 2872 showCancelButton: true, 2873 animation: false, 2874 confirmButtonText: gl("swal_btnyes") || "Sim", 2875 cancelButtonText: gl("swal_btncancel") || "Cancelar", 2876 closeOnConfirm: true 2877 }, function (confirmed) { 2878 if (confirmed) removeItem(); 2879 }); 2880 } else if (confirm(gl("swal_action_will_delete_item") || "Remover este item?")) { 2881 removeItem(); 2882 } 2883 }); 2884 } 2885 2886 function enableMobileStickyNav() { 2887 var navWrapper = document.querySelector(".catalog-top-wrapper"); 2888 var sentinel = document.getElementById("catalog_top"); 2889 if (!navWrapper || !sentinel) return; 2890 2891 var togglePinned = function (isPinned) { 2892 navWrapper.classList.toggle("catalog-top-wrapper--pinned", !!isPinned); 2893 document.body.classList.toggle("catalog-sticky-active", !!isPinned); 2894 }; 2895 2896 if ("IntersectionObserver" in window) { 2897 var observer = new IntersectionObserver( 2898 function (entries) { 2899 var entry = entries[0]; 2900 togglePinned(entry.intersectionRatio === 0 && entry.boundingClientRect.top < 0); 2901 }, 2902 { threshold: [0, 1] } 2903 ); 2904 observer.observe(sentinel); 2905 } else { 2906 var sentinelOffset = sentinel.getBoundingClientRect().top + window.pageYOffset; 2907 var onScroll = function () { 2908 togglePinned(window.pageYOffset > sentinelOffset); 2909 }; 2910 window.addEventListener("scroll", onScroll); 2911 onScroll(); 2912 } 2913 } 2914 2915 2916 2917 /* ===== Tools Popover (LÃngua & Câmbio) ===== */ 2918 function bindToolsPopover() { 2919 const $btn = $("#tools-container"); 2920 const $pop = $("#tools-options"); 2921 if (!$btn.length || !$pop.length) return; 2922 2923 // Move para o body apenas uma vez 2924 if (!$pop.data("ported")) { 2925 $pop 2926 .appendTo("body") 2927 .data("ported", true) 2928 .hide() 2929 .css({ 2930 position: "absolute", 2931 zIndex: 9999, 2932 width: 165, // fixa a largura máxima desejada 2933 maxWidth: 165, // impede que cresça 2934 minWidth: 165, // mantém consistência 2935 }) 2936 .addClass("tools-popover"); 2937 } 2938 2939 function place() { 2940 const rect = $btn[0].getBoundingClientRect(); 2941 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 2942 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; 2943 2944 const btnW = rect.width;
2945 const btnH = rect.height; 2946 2947 const popW = 162; 2948 $pop.css({ width: popW, maxWidth: popW, minWidth: popW, margin: 0 }); 2949 2950 // alinhado pela borda direita + pequeno espaçamento 2951 const left = rect.left + scrollLeft + btnW - popW + 8; 2952 const top = rect.top + scrollTop + btnH + 8; 2953 2954 // (sem clamping automático) 2955 $pop.css({ top: top, left: left }); 2956 } 2957 2958 function show() { 2959 place(); 2960 $("#shopcart-modal-container").hide(); 2961 $pop.stop(true, true).fadeIn(100); 2962 } 2963 2964 function hide() { 2965 $pop.stop(true, true).fadeOut(80); 2966 } 2967 2968 // Limpa handlers antigos 2969 $(document).off(".toolsPopover"); 2970 $btn.off(".toolsPopover"); 2971 $pop.off(".toolsPopover"); 2972 2973 // Click para abrir/fechar 2974 $btn.on("click.toolsPopover", function (e) { 2975 e.preventDefault(); 2976 e.stopPropagation(); 2977 $pop.is(":visible") ? hide() : show(); 2978 }); 2979 2980 // Fecha ao clicar fora 2981 $(document).on("click.toolsPopover", function (e) { 2982 if (!$(e.target).closest("#tools-options, #tools-container").length) 2983 hide(); 2984 }); 2985 2986 // Reposiciona se estiver aberto 2987 $(window).on("scroll.toolsPopover resize.toolsPopover", function () { 2988 if ($pop.is(":visible")) place(); 2989 }); 2990 } 2991 2992 bindToolsPopover(); 2993 2994 /* ========================================================== 2995 * 6) PESQUISA 2996 * ========================================================== */ 2997 (function () { 2998 var $q = $("#q"); if ($q.length) { 2999 $q.on("keydown", function (e) { 3000 if (e.key === "Enter") { 3001 e.preventDefault(); 3002 var t = $.trim($q.val() || ""); if (!t) return; 3003 var $f = $q.closest("form"); if ($f.length) $f.trigger("submit"); 3004 else location.href = "/search/?q=" + encodeURIComponent(t); 3005 } 3006 }); 3007 } 3008 })(); 3009 3010 /* ========================================================== 3011 * 7) DATA LAYER / ANALYTICS E OUTROS HANDLERS GERAIS 3012 * ========================================================== */ 3013 $(function () { 3014 // Fancybox básico + alert toggle 3015 if ($.fancybox) $(".fancybox").fancybox(); 3016 $(document).on("click.ux", ".alert .toggle-alert", function () { 3017 $(this).closest(".alert").slideUp(); return false; 3018 }); 3019 3020 // Lazy 3021 if ($.fn.lazy) { 3022 $(".lazy").Lazy({ scrollDirection: "vertical", effect: "fadeIn", effectTime: 500, threshold: 0, visibleOnly: true }); 3023 if ($.fn.lazy) $(".lazy-throttle").lazy({ enableThrottle: true, throttle: 250, effect: "fadeIn", effectTime: 500, threshold: 0 }); 3024 } 3025 3026 // Botões de evento 3027 $(".btnGreenLargeSH").on("click", function () { 3028 window.dataLayer = window.dataLayer || []; 3029 window.dataLayer.push({ event: 'begin_checkout', event_category: 'ecommerce', event_label: 'Checkout iniciado' }); 3030 }); 3031 $("#shopcart_shopping_button").on("click", function () { 3032 window.dataLayer = window.dataLayer || []; 3033 window.dataLayer.push({ event: 'continue_shopping', event_category: 'ecommerce', event_label: 'Botão continuar a comprar' }); 3034 }); 3035 3036 // Scroll depth (25/50/75/100) 3037 (function () { 3038 window.dataLayer = window.dataLayer || []; 3039 var thresholds = [25, 50, 75, 100], triggered = {}; 3040 function throttle(fn, delay) { var last = 0; return function () { var now = Date.now(); if (now - last < delay) return; last = now; fn.apply(this, arguments); }; } 3041 function onScroll() { 3042 var y = window.scrollY || window.pageYOffset; 3043 var doc = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight); 3044 var wh = window.innerHeight; 3045 var p = Math.round((y / (doc - wh)) * 100); 3046 thresholds.forEach(function (t) { 3047 if (p >= t && !triggered[t]) { triggered[t] = 1; window.dataLayer.push({ event: 'scroll', scroll_threshold: t }); } 3048 }); 3049 } 3050 window.addEventListener('scroll', throttle(onScroll, 200)); 3051 })(); 3052 }); 3053 3054 /* ========================================================== 3055 * 8) LOGIN / GOOGLE OAUTH / MISC LEGADO 3056 * ========================================================== */ 3057 function generate_cupidovip(uid) { 3058 if (!uid) return; 3059 3060 $.ajax({ 3061 type: "POST", 3062 url: REQUEST_ENDPOINT, 3063 cache: false, 3064 data: { 3065 ajax: "shopcart_cupidovip", 3066 uid: uid 3067 } 3068 }).done(function (html) {
3069 $("#shopcart_login, #account_login, #shop_cart_table_form").hide(); 3070 $("#shopcart_cupidovip").html(html).fadeIn(700); 3071 try { 3072 if (typeof window.cv2MaybeOpenIncompleteAccount === "function") { 3073 window.setTimeout(window.cv2MaybeOpenIncompleteAccount, 80); 3074 window.setTimeout(window.cv2MaybeOpenIncompleteAccount, 400); 3075 } else if ($("#cacc_form").length) { 3076 var addBtns = $("#cacc_view .checkout-account__field--phone [data-cacc-edit], #cacc_view .checkout-account__field--addr [data-cacc-edit]"); 3077 if (addBtns.length) { 3078 $("#cacc_view").hide(); 3079 $("#cacc_form").show(); 3080 } 3081 } 3082 } catch (eVipOpen) {} 3083 }); 3084 } 3085 window.generate_cupidovip = generate_cupidovip; 3086 3087 function logoutUser() { 3088 $.post(REQUEST_ENDPOINT, { ajax: "logout" }, function (res) { 3089 var r; 3090 try { r = typeof res === "object" ? res : JSON.parse(res); } 3091 catch (err) { r = {}; } 3092 3093 if (r && r.googleLogout) { 3094 var logoutUrl = "https://accounts.google.com/Logout"; 3095 var backToSite = "https://www.cupidosshop.com/logout.php"; 3096 3097 var iframe = document.createElement("iframe"); 3098 iframe.style.display = "none"; 3099 iframe.src = logoutUrl; 3100 document.body.appendChild(iframe); 3101 3102 setTimeout(function () { 3103 window.location.href = backToSite; 3104 }, 500); 3105 } else { 3106 window.location.reload(); 3107 } 3108 }); 3109 } 3110 window.logout = logoutUser; 3111 3112 function cttBuildExpedicao() { 3113 $.post(REQUEST_ENDPOINT, { ajax: "cttBuildExpedicao" }); 3114 } 3115 3116 var accountRecaptchaRendered = false; 3117 var accountRecaptchaTries = 0; 3118 3119 function ensureAccountRecaptcha() { 3120 var $container = $("#g-recaptcha"); 3121 if (!$container.length) return; 3122 if (accountRecaptchaRendered) return; 3123 if (typeof grecaptcha === "undefined" || !grecaptcha.render) { 3124 if (accountRecaptchaTries < 20) { 3125 accountRecaptchaTries++; 3126 setTimeout(ensureAccountRecaptcha, 500); 3127 } 3128 return; 3129 } 3130 3131 grecaptcha.render("g-recaptcha", { 3132 sitekey: "6LetpiEUAAAAAOrCNcPBkwobhVBEjjhAi9IkEYVr", 3133 callback: function (token) { 3134 $("#g-recaptcha-response").val(token); 3135 } 3136 }); 3137 accountRecaptchaRendered = true; 3138 } 3139 3140 function populateAccountForm() { 3141 if (!$("#account_table_form").length) return; 3142 $.ajax({ 3143 type: "POST", 3144 url: REQUEST_ENDPOINT, 3145 cache: false, 3146 data: { ajax: "getUserData" } 3147 }).done(function (resp) { 3148 var data = {}; 3149 try { data = typeof resp === "object" ? resp : JSON.parse(resp); } 3150 catch (err) { data = {}; } 3151 3152 $.each(data, function (key, value) { 3153 var $el = $("#" + key); 3154 if (!$el.length) return; 3155 3156 if ($el.is(":checkbox")) { 3157 var isChecked = (value === "on" || value === "1" || value === 1 || value === true); 3158 $el.prop("checked", isChecked); 3159 } else { 3160 $el.val(value); 3161 } 3162 }); 3163 3164 setTimeout(function () { 3165 $("#newsletter").prop("checked", data.newsletter === "on" || data.newsletter === "1" || data.newsletter === 1); 3166 $("#newsletter_swing").prop("checked", data.newsletter_swing === "on" || data.newsletter_swing === "1" || data.newsletter_swing === 1); 3167 }, 300); 3168 3169 if (data.birthday) { 3170 $("#birthday").prop("disabled", true); 3171 } 3172 }); 3173 } 3174 3175 function initAccountValidation() { 3176 if (typeof $.fn.validate === "undefined") return; 3177 var $forms = $("#account_registration, #account_update"); 3178 if (!$forms.length) return; 3179 cv2EnsurePhoneValidator(); 3180 3181 $forms.validate({ 3182 ignore: ":not(:visible)", 3183 focusInvalid: false, 3184 rules: { 3185 nome: { required: true }, 3186 email: { required: true, email: true }, 3187 email2: { required: true, equalTo: "#email" }, 3188 password: { required: true, minlength: 6 }, 3189 password2: { equalTo: "#password" }, 3190 morada: { required: true }, 3191 localidade: { required: true }, 3192 codigo_postal: { required: true }, 3193 telefone: { required: true, cv2phone: true }, 3194 sexo: { required: true } 3195 /* t1409: birthday opcional na conta */ 3196 }, 3197 invalidHandler: function (evt, validator) { 3198 /* t1589: sem popup â só highlight + scroll/foco no 1.º campo inválido */ 3199 if (validator && validator.errorList && validator.errorList.length) { 3200 var el = validator.errorList[0].element; 3201 $("html, body").animate({ scrollTop: $(el).offset().top - 110 }, 1000); 3202 $(el).focus(); 3203 } 3204 }, 3205 onkeyup: function (element) {
3206 $(element).removeClass("LV_invalid_input"); 3207 }, 3208 onfocusout: function (element) { 3209 var $el = $(element); 3210 if ($el.valid && $el.valid()) { 3211 $el.removeClass("LV_invalid_input"); 3212 } else { 3213 $el.addClass("LV_invalid_input"); 3214 } 3215 }, 3216 messages: (window.cv2Jqv && cv2Jqv.fieldMessages) ? cv2Jqv.fieldMessages() : {}, 3217 errorPlacement: function (error, element) { 3218 if (window.cv2Jqv && cv2Jqv.errorPlacement) return cv2Jqv.errorPlacement(error, element); 3219 error.insertAfter(element); 3220 }, 3221 showErrors: function (errorMap, errorList) { 3222 if (window.cv2Jqv && cv2Jqv.showErrorsWithLabels) { 3223 return cv2Jqv.showErrorsWithLabels.call(this, errorMap, errorList); 3224 } 3225 $.each(errorList, function (_, item) { 3226 $(item.element).addClass("LV_invalid_input"); 3227 }); 3228 this.defaultShowErrors(); 3229 } 3230 }); 3231 } 3232 3233 function getAccountFormPayload() { 3234 return { 3235 email: $("#email").val(), 3236 password: $("#password").val(), 3237 nome: $("#nome").val(), 3238 morada: $("#morada").val(), 3239 fiscal_id: $("#fiscal_id").val(), 3240 localidade: $("#localidade").val(), 3241 codigo_postal: $("#codigo_postal").val(), 3242 country: $("#country").val(), 3243 telefone: $("#telefone").val(), 3244 sexo: $("#sexo").val(), 3245 birthday: $("#birthday").val(), 3246 newsletter: $("#newsletter").is(":checked") ? "1" : "0", 3247 newsletter_swing: $("#newsletter_swing").is(":checked") ? "1" : "0", 3248 "g-recaptcha-response": $("#g-recaptcha-response").val() 3249 }; 3250 } 3251 3252 function resetSubmitButton($submitButton) { 3253 if ($submitButton && $submitButton.length) { 3254 $submitButton.prop("disabled", false) 3255 .html($submitButton.data("original-text") || $submitButton.text()); 3256 } 3257 } 3258 3259 function submitAccountForm(action, $submitButton) { 3260 if (typeof grecaptcha !== "undefined" && !grecaptcha.getResponse()) { 3261 if (typeof swal === "function") { 3262 swal({ 3263 title: gl("swal_attention") || "Atenção", 3264 text: gl("RECAPTCHAERR") || "Confirme o reCAPTCHA.", 3265 type: "warning", 3266 confirmButtonColor: "#ABD559",
3267 showCancelButton: false, 3268 animation: false, 3269 confirmButtonText: gl("swal_btnok") || "OK", 3270 closeOnConfirm: true 3271 }); 3272 } 3273 resetSubmitButton($submitButton); 3274 return; 3275 } 3276 3277 var payload = getAccountFormPayload(); 3278 var lang = $("#session_lang").val() || "pt"; 3279 3280 $.ajax({ 3281 type: "POST", 3282 url: REQUEST_ENDPOINT, 3283 cache: false, 3284 data: { ajax: action, formData: payload } 3285 }).done(function (res) { 3286 switch (String(res)) { 3287 case "0": 3288 swal({ 3289 title: gl("swal_attention") || "Atenção", 3290 text: gl("NEWUSERERR") || "Ocorreu um erro ao criar a conta.", 3291 type: "error", 3292 confirmButtonColor: "#ABD559", 3293 showCancelButton: false, 3294 animation: false, 3295 confirmButtonText: gl("swal_btnok") || "OK", 3296 closeOnConfirm: true 3297 }, function () { window.location.href = "/" + lang + "/"; }); 3298 break; 3299 case "1": 3300 swal({ 3301 title: gl("swal_info") || "Informação", 3302 text: gl("NEWUSEROK") || "Conta criada com sucesso.", 3303 type: "success", 3304 confirmButtonColor: "#ABD559",
3305 showCancelButton: false, 3306 animation: false, 3307 confirmButtonText: gl("swal_btnok") || "OK", 3308 closeOnConfirm: true 3309 }, function () { window.location.href = "/" + lang + "/"; }); 3310 break; 3311 case "3": 3312 swal({ 3313 title: gl("swal_info") || "Informação", 3314 text: gl("NEWUSERDUP") || "Este e-mail já está registado.", 3315 type: "warning", 3316 confirmButtonColor: "#ABD559", 3317 showCancelButton: false, 3318 animation: false, 3319 confirmButtonText: gl("swal_btnok") || "OK", 3320 closeOnConfirm: true 3321 }, function () { window.location.href = "/" + lang + "/"; }); 3322 break; 3323 case "mail-err": 3324 swal({ 3325 title: gl("swal_attention") || "Atenção", 3326 text: gl("EMAILNOK") || "O e-mail que introduziu parece não existir, por favor valide o campo E-mail.", 3327 type: "warning", 3328 confirmButtonColor: "#ABD559", 3329 showCancelButton: fal
3329se, 3330 animation: false, 3331 confirmButtonText: gl("swal_btnok") || "OK", 3332 closeOnConfirm: true 3333 }); 3334 break; 3335 case "4": 3336 swal({ 3337 title: gl("swal_info") || "Informação", 3338 text: gl("UPDATEUSEROK") || "Dados atualizados com sucesso.", 3339 type: "success", 3340 confirmButtonColor: "#ABD559", 3341 showCancelButton: false, 3342 animation: false, 3343 confirmButtonText: gl("swal_btnok") || "OK", 3344 closeOnConfirm: true 3345 }, function () { window.location.href = "/" + lang + "/"; }); 3346 break; 3347 case "recaptcha-err": 3348 swal({ 3349 title: gl("swal_attention") || "Atenção", 3350 text: gl("RECAPTCHAHACK") || "Confirmação reCAPTCHA inválida.", 3351 type: "warning", 3352 confirmButtonColor: "#ABD559", 3353 showCancelButton: false, 3354 animation: false, 3355 confirmButtonText: gl("swal_btnok") || "OK", 3356 closeOnConfirm: true 3357 }, function () { window.location.href = "/" + lang + "/account/"; }); 3358 break; 3359 default: 3360 toastr.info(gl("UPDATEUSEROK") || "Operação concluÃda."); 3361 break; 3362 } 3363 }).always(function () { 3364 resetSubmitButton($submitButton); 3365 }); 3366 } 3367 3368 function initOrdersTable() { 3369 if (!$.fn.DataTable) return; 3370 var $table = $("#orders_table"); 3371 if (!$table.length || $table.hasClass("dt-initialized")) return; 3372 3373 $table.on("xhr.dt", function () { /* placeholder for potential hooks */ }); 3374 3375 $table.DataTable({ 3376 responsive: true, 3377 processing: true, 3378 serverSide: true, 3379 searching: true, 3380 dom: "flrBtip", 3381 lengthMenu: [[10, 25, 100, -1], [10, 25, 100, "All"]], 3382 pageLength: 10, 3383 ajax: "/customer-dt-orders.php" 3384 }); 3385 3386 if (typeof $.fn.dataTable !== "undefined" && $.fn.dataTable.ext && $.fn.dataTable.ext.search) { 3387 // Retain legacy delay helper if available 3388 if ($.fn.dataTable && $.fn.dataTableExt && $.fn.dataTableExt.oApi && $.fn.dataTableExt.oApi.fnSetFilteringDelay) { 3389 $table.dataTable().fnSetFilteringDelay(600); 3390 } 3391 } 3392 3393 $table.addClass("dt-initialized"); 3394 } 3395 3396 function renderAccountLogin() { 3397 return $.ajax({ 3398 type: "POST", 3399 url: REQUEST_ENDPOINT, 3400 cache: false, 3401 data: { ajax: "show_login" } 3402 }).done(function (html) { 3403 var $login = $("#account_login"); 3404 if (!$login.length) return; 3405 $login.html(html).show(); 3406 var $form = $("#login_cupidovip"); 3407 if ($form.length) { 3408 $form.off("submit.accountLogin").on("submit.accountLogin", function (evt) { 3409 evt.preventDefault(); 3410 processLogin(); 3411 }); 3412 } 3413 }); 3414 } 3415 3416 function renderAccountForm() { 3417 return $.ajax({ 3418 type: "POST", 3419 url: REQUEST_ENDPOINT, 3420 cache: false, 3421 data: { ajax: "show_register_form" } 3422 }).done(function (html) { 3423 var $wrap = $("#account_table_form"); 3424 if (!$wrap.length) return; 3425 3426 $wrap.hide().empty().html(html).fadeIn(200); 3427 3428 var $reg = $("#account_registration"); 3429 if ($reg.length) { 3430 $reg.off("submit.accountRegister").on("submit.accountRegister", function (evt) { 3431 evt.preventDefault(); 3432 var $btn = $("#account_registration_submit"); 3433 if ($reg.valid && !$reg.valid()) { 3434 resetSubmitButton($btn); 3435 return; 3436 } 3437 if ($btn.length) { 3438 $btn.data("original-text", $btn.html()).html('<i class="fa fa-cog fa-spin" style="color:inherit;"></i>').prop("disabled", true); 3439 } 3440 submitAccountForm("registerUser", $btn); 3441 }); 3442 } 3443 3444 var $update = $("#account_update"); 3445 if ($update.length) { 3446 $update.off("submit.accountUpdate").on("submit.accountUpdate", function (evt) { 3447 evt.preventDefault(); 3448 var $btn = $("#account_update_submit"); 3449 if ($update.valid && !$update.valid()) { 3450 resetSubmitButton($btn); 3451 return; 3452 } 3453 if ($btn.length) { 3454 $btn.data("original-text", $btn.html()).html('<i class="fa fa-cog fa-spin" style="color:inherit;"></i>').prop("disabled", true); 3455 } 3456 submitAccountForm("updateUser", $btn); 3457 }); 3458 } 3459 3460 $(document) 3461 .off("click.accountRegistrationSubmit") 3462 .on("click.accountRegistrationSubmit", "#account_registration_submit", function () { 3463 $("#account_registration").trigger("submit"); 3464 }); 3465 3466 $(document) 3467 .off("click.accountUpdateSubmit") 3468 .on("click.accountUpdateSubmit", "#account_update_submit", function () { 3469 $("#account_update").trigger("submit"); 3470 }); 3471 3472 initAccountValidation(); 3473 populateAccountForm(); 3474 ensureAccountRecaptcha(); 3475 initOrdersTable(); 3476 /* t1623: reiniciar Select2 paÃs após HTML AJAX da área de conta */
3477 try { 3478 if (typeof window.cv2CountrySelect2Init === "function") { 3479 window.cv2CountrySelect2Init(); 3480 } 3481 } catch (eC) {} 3482 try { 3483 $("#account_table_form, #account_table_form .user_area_div, #account_update, #account_registration") 3484 .css("overflow", "visible"); 3485 } catch (eO) {} 3486 }); 3487 } 3488 3489 var accountDelegatesBound = false; 3490 function bindAccountDelegates() { 3491 if (accountDelegatesBound) return; 3492 accountDelegatesBound = true; 3493 3494 $(document) 3495 .off("click.accountRecover") 3496 .on("click.accountRecover", "#cupidovip_password_recovery", function () { 3497 if (typeof swal !== "function") return; 3498 swal({ 3499 title: gl("swal_recoverpass_title") || "Recuperar password", 3500 text: gl("swal_recoverpass_message") || "Insira o seu e-mail.", 3501 type: "input", 3502 showCancelButton: true, 3503 closeOnConfirm: false, 3504 cancelButtonText: gl("swal_btncancel") || "Cancelar", 3505 confirmButtonText: gl("swal_btnok") || "OK", 3506 animation: false, 3507 showLoaderOnConfirm: true, 3508 inputPlaceholder: "E-mail" 3509 }, function (value) { 3510 if (value === false) return false; 3511 if (!value) { 3512 swal.showInputError(gl("swal_recoverpass_alert") || "Introduza um e-mail válido."); 3513 return false; 3514 } 3515 3516 if (typeof checkifemail === "function" && checkifemail(value) === false) { 3517 swal.showInputError(gl("swal_invalid_email") || "E-mail inválido."); 3518 return false; 3519 } 3520 3521 $.ajax({ 3522 type: "POST", 3523 url: REQUEST_ENDPOINT, 3524 cache: false, 3525 data: { ajax: "shopcart_recover", email: value } 3526 }).done(function (res) { 3527 /* t1623: anti-enumeração â nunca revelar se o email existe */ 3528 if (res === "ERROR") { 3529 swal.showInputError(gl("swal_email_blocked") || "E-mail bloqueado."); 3530 } else { 3531 var genMsg = gl("CV2_OTP_GENERIC_SENT") || "Receberá um email se o mesmo estiver registado."; 3532 swal(gl("swal_email_recovertitle") || "Pedido recebido", genMsg, "success"); 3533 } 3534 }); 3535 }); 3536 }); 3537 3538 $(document) 3539 .off("focusout.accountEmail") 3540 .on("focusout.accountEmail", "#account_registration #email", function () { 3541 var email = $(this).val(); 3542 if (!email) return; 3543 $.ajax({ 3544 type: "POST", 3545 url: REQUEST_ENDPOINT, 3546 cache: false, 3547 data: { ajax: "checkEmailWebstore", email: email } 3548 }).done(function (res) { 3549 if (parseInt(res, 10) === 1 && typeof swal === "function") { 3550 swal({ 3551 title: gl("swal_info_cupidovip_alert") || "Já existe conta", 3552 text: gl("swal_info_cupidovip_message") || "Este e-mail já está registado.", 3553 type: "info", 3554 confirmButtonColor: "#ABD559",
3555 showCancelButton: false, 3556 animation: false, 3557 confirmButtonText: gl("swal_btnok") || "OK", 3558 closeOnConfirm: true 3559 }, function () { 3560 $("#usercesto").val(email); 3561 $("#email, #email2").val("").removeClass("LV_invalid_input"); 3562 $("#usercesto").removeClass("LV_invalid_input"); 3563 $("#passcesto").focus().removeClass("LV_invalid_input"); 3564 }); 3565 } 3566 }); 3567 }); 3568 3569 /* t1605: checkout â aviso BO-i18n, sem limpar o email (convidado permitido) */ 3570 $(document) 3571 .off("focusout.checkoutEmailExists click.checkoutEmailExists") 3572 .on("focusout.checkoutEmailExists", "#encomenda #email", function () { 3573 var email = $.trim($(this).val() || ""); 3574 var $hint = $("#cv2_email_exists_hint"); 3575 if (!$hint.length) return; 3576 if (!email || email.indexOf("@") < 1) { 3577 $hint.prop("hidden", true); 3578 return; 3579 } 3580 $.ajax({ 3581 type: "POST", 3582 url: REQUEST_ENDPOINT, 3583 cache: false, 3584 data: { ajax: "checkEmailWebstore", email: email } 3585 }).done(function (res) { 3586 if (parseInt(res, 10) === 1) { 3587 $hint.prop("hidden", false); 3588 if ($("#cv2_create_account").length && $("#cv2_create_account").is(":checked")) { 3589 $("#cv2_create_account").prop("checked", false).trigger("change"); 3590 } 3591 } else { 3592 $hint.prop("hidden", true); 3593 } 3594 }); 3595 }) 3596 .on("click.checkoutEmailExists", "#cv2_email_exists_login", function (e) { 3597 e.preventDefault(); 3598 var email = $.trim($("#encomenda #email").val() || ""); 3599 if (email) { 3600 $("#usercesto, #cv2_otp_email").val(email); 3601 } 3602 $("#cv2_open_login").trigger("click"); 3603 }) 3604 .on("click.checkoutEmailExists", "#cv2_email_exists_dismiss", function (e) { 3605 e.preventDefault(); 3606 $("#cv2_email_exists_hint").prop("hidden", true); 3607 }); 3608 } 3609 3610 // o link "Recuperar acesso" tambem existe no painel de login do checkout â 3611 // os delegados sao globais (document) e idempotentes, ligar sempre 3612 bindAccountDelegates(); 3613 3614 function initAccountSection() { 3615 if (!$("#account_html").length) return; 3616 bindAccountDelegates(); 3617 3618 $.ajax({ 3619 type: "POST", 3620 url: REQUEST_ENDPOINT, 3621 cache: false, 3622 data: { ajax: "checkUid" } 3623 }).done(function (res) { 3624 var isLogged = parseInt(res, 10) !== 0; 3625 if (!isLogged) { 3626 renderAccountLogin(); 3627 } 3628 renderAccountForm(); 3629 }); 3630 } 3631 3632 window.handleCredentialResponse = function (response) { 3633 // CV3 (2026-08-07): o id_token do Google e validado no proprio site 3634 // (google-oauth.php valida com a Google e resolve o utilizador). Deixa de 3635 // haver o hop ao backend de producao nem um uid cru vindo do browser. 3636 $.post("/google-oauth.php", { credential: response.credential }, function (authRes) { 3637 var auth;
3638 try { auth = typeof authRes === "object" ? authRes : JSON.parse(authRes); } 3639 catch (e) { swal("Erro", "Resposta inválida do servidor.", "error"); return; } 3640 if (auth && auth.success) window.location.reload(); 3641 else swal("Erro", "Falha ao validar o login Google.", "error"); 3642 }); 3643 }; 3644 3645 function processLogin() { 3646 $.ajax({ 3647 type: "POST", cache: false, url: REQUEST_ENDPOINT, 3648 data: { ajax: "makeLogin", usercesto: $("#usercesto").val(), passcesto: $("#passcesto").val(), csrf: window.cv2Csrf || "" }, 3649 success: function (e) { 3650 var t = JSON.parse(e); 3651 if (t.csrf) window.cv2Csrf = t.csrf; 3652 if (t.failed) { 3653 swal({ 3654 title: gl("swal_attention"), 3655 text: gl("swal_login_failed"), 3656 type: "info", confirmButtonColor: "#ABD559", 3657 showCancelButton: false, animation: false, 3658 confirmButtonText: gl("swal_btnok"), closeOnConfirm: true 3659 }, function (confirm) { 3660 if (confirm) { $("#passcesto").val(""); $("#usercesto").removeClass("LV_invalid_input"); $("#passcesto").removeClass("LV_invalid_input"); $("#usercesto").focus(); } 3661 }); 3662 return; 3663 } 3664 $("#uid").val(t.id); 3665 generate_cupidovip(t.id); 3666 toastr.info(gl("CUPIDOVIP_LOGIN")); 3667 if ($("#shopcart_html").length) { populate_shopcart(); shopcart_specialservices(); } 3668 if ($("#account_html").length) { var lang = $("#session_lang").val(); window.location.href = "/" + lang + "/account/"; } 3669 } 3670 }); 3671 } 3672 3673 /* ========================================================== 3674 * 9) ENTREGA / MAPAS / SHOPCART 3675 * ========================================================== */ 3676 function clickPayMethod(id) { 3677 $("#paymethods_sibs, #paymethods_sibs_mb, #paymethods_sibs_mbway, #paymethods_paypal_image, #western_image").addClass("desaturate"); 3678 switch (id) { 3679 case 'sc_payment_sibs_card': $("#paymethods_sibs").removeClass("desaturate"); break; 3680 case 'sc_payment_sibs_mb': $("#paymethods_sibs_mb").removeClass("desaturate"); break; 3681 case 'sc_payment_sibs_mbway': $("#paymethods_sibs_mbway").removeClass("desaturate"); break; 3682 case 'sc_payment_api_paypal': $("#paymethods_paypal_image").removeClass("desaturate"); break; 3683 case 'sc_payment_western': $("#western_image").removeClass("desaturate"); break; 3684 } 3685 $(".paymethod").removeClass("shopcart_paytype_button_selected"); 3686 $("#" + id).addClass("shopcart_paytype_button_selected"); 3687 } 3688 3689 function clickDeliveryMethod(e) { 3690 switch (e) { 3691 case "sc_delivery_home": 3692 $("#ctt_image,#worten_image").addClass("desaturate"); 3693 $("#selected_deliverymethod_aux").val("sc_delivery_home"); 3694 $("#delivery_method_api,#delivery_api_output,#shop_cart_table_delivery").slideUp(); 3695 $("#sc_payment_reception").show(); 3696 break; 3697 case "sc_delivery_alternative": 3698 $("#ctt_image,#worten_image").addClass("desaturate"); 3699 $("#selected_deliverymethod_aux").val("sc_delivery_alternative"); 3700 $("#delivery_method_api,#delivery_api_output").slideUp(); 3701 $("#shop_cart_table_delivery").fadeIn(700); 3702 $("html, body").animate({ scrollTop: $("#shop_cart_table_delivery").position().top - 110 }, "slow"); 3703 $("#delivery_nome").focus(); 3704 $("#sc_payment_reception").show(); 3705 break; 3706 case "sc_delivery_ctt": 3707 $("#ctt_image").removeClass("desaturate"); $("#worten_image").addClass("desaturate"); 3708 if (!/^(sc_delivery_ctt|sc_delivery_worten)$/.test($("#selected_deliverymethod_aux").val())) $("#delivery_method_api").slideUp(); 3709 $("#selected_deliverymethod_aux").val("sc_delivery_ctt"); 3710 $("#delivery_api_output").hide(); $("#shop_cart_table_delivery").slideUp(); 3711 callAPIresults(2); $("#sc_payment_reception").hide(); break; 3712 case "sc_delivery_worten": 3713 $("#worten_image").removeClass("desaturate"); $("#ctt_image").addClass("desaturate"); 3714 if (!/^(sc_delivery_ctt|sc_delivery_worten)$/.test($("#selected_deliverymethod_aux").val())) $("#delivery_method_api").slide
3714Up(); 3715 $("#selected_deliverymethod_aux").val("sc_delivery_worten"); 3716 $("#delivery_api_output").hide(); $("#shop_cart_table_delivery").slideUp(); 3717 callAPIresults(1); $("#sc_payment_reception").hide(); break; 3718 } 3719 $(".deliverymethod").removeClass("shopcart_delivery_button_selected"); 3720 $("#" + e).addClass("shopcart_delivery_button_selected"); 3721 // o metodo de entrega muda a tabela de portes (ex.: ponto CTT) â refrescar resumo 3722 if (typeof populate_shopcart === "function") populate_shopcart(); 3723 } 3724 3725 function callAPIresults(e, t) { 3726 var langctt = $("#session_lang").val(); 3727 if (e == "pt") { langctt = "pt"; e = 1; } 3728 if (e == "es") { langctt = "es"; e = 1; } 3729 3730 if (!$("#shopcart-api").length) return; 3731 3732 // Pesquisa server-side (getCTT_search): o select2 pede so os resultados do termo. 3733 // O fluxo antigo carregava todas as options (api_ctt_es >22k = 1.4MB) e congelava o browser. 3734 var $points = $("#api_points"); 3735 if ($points.data("select2")) $points.select2("destroy"); 3736 $points.empty().prop("disabled", false); 3737 3738 // Mudar de pais faz reset a escolha anterior (ponto + painel de detalhes) 3739 $("#delivery_api_output").hide(); 3740 $("#api-selected-html").empty(); 3741 $("#api-selected-address").empty(); 3742 $("#shopcart-api-txt").empty(); 3743 3744 $points.select2({ 3745 minimumInputLength: 2, 3746 language: { 3747 inputTooShort: function () { return gl("CV2_CTT_TYPE2") || "Escreva pelo menos 2 letras"; }, 3748 searching: function () { return gl("CV2_CTT_SEARCHING") || "A procurar..."; }, 3749 noResults: function () { return gl("CV2_CTT_NORESULTS") || "Sem resultados"; } 3750 }, 3751 ajax: { 3752 url: REQUEST_ENDPOINT, 3753 type: "POST", 3754 dataType: "json", 3755 delay: 250, 3756 data: function (params) { 3757 return { ajax: "getCTT_search", lang: langctt, term: params.term }; 3758 }, 3759 processResults: function (data) { return data && data.results ? data : { results: [] }; }, 3760 cache: true 3761 } 3762 }); 3763 3764 // Ao escolher um ponto, mostrar logo os detalhes (binding em falta no fluxo antigo) 3765 $(document).off("change.cttPoint", "#api_points").on("change.cttPoint", "#api_points", function () { 3766 var v = $(this).val(); 3767 if (v) triggerAPIcall(v); 3768 }); 3769 3770 $("#delivery_method_api").fadeIn(700); 3771 if (jQuery.browser && jQuery.browser.mobile) $("html, body").animate({ scrollTop: $("#delivery_method_api").position().top - 120 }, "slow"); 3772 3773 if (t) { 3774 $points.append(new Option("CTT", t, true, true)).trigger("change"); 3775 } else { 3776 $points.select2("open"); 3777 $(".select2-search__field").trigger("click").focus(); 3778 } 3779 } 3780 window.callAPIresults = callAPIresults; 3781 3782 // \b\w quebrava em acentos ("Algés" -> "AlgéS"); so capitaliza no inicio de palavra real 3783 function ucword(str) { return str.toLowerCase().replace(/(^|[\s(\/\-])[a-zà -öø-ÿ]/g, function (l) { return l.toUpperCase(); }); } 3784 3785 function triggerAPIcall(selectedPaymentMethod) { 3786 $("#selected_paymethod_aux_select").val(selectedPaymentMethod); 3787 var lang_ctt = (document.getElementById('ctt_lang_es') && document.getElementById('ctt_lang_es').checked) ? "es" : "pt"; 3788 3789 if (!selectedPaymentMethod) return; 3790 3791 $.ajax({ 3792 type: "POST", async: true, cache: false, url: REQUEST_ENDPOINT, dataType: "json", 3793 data: { ajax: "getAPIdetail", lang: lang_ctt, ctt_id: selectedPaymentMethod }, 3794 success: function (response) { 3795 var latitude = response.latitude, longitude = response.longitude, town = response.town, 3796 pointName = response.point_name, streetName = response.street_name, postalcode = response.postalcode, 3797 schedule = response.schedule, phone = response.phone, tipo = response.tipo; 3798 3799 var formattedStreetName = ucword((streetName || "").toLowerCase()); 3800 var formattedPointName = ucword((pointName || "").toLowerCase()); 3801 var formattedSchedule = schedule || ""; 3802 var cttCountryLabel = (lang_ctt === "es") ? "España" : "Portugal"; 3803 var cttLocalidade = postalcode + " " + town; 3804 if (String(town).toLowerCase().indexOf(cttCountryLabel.toLowerCase()) === -1) cttLocalidade += ", " + cttCountryLabel; 3805 var cttPhone = String(phone || "").replace(/^\s*tel\.?:?\s*/i, ""); 3806 3807 var metaBits = []; 3808 if (formattedSchedule) { 3809 metaBits.push("<span class='cv2-ctt-metaitem'><svg viewBox='0 0 24 24' width='15' height='15' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' aria-hidden='true'><circle cx='12' cy='12' r='9'/><path d='M12 7v5l3 2'/></svg>" + formattedSchedule + "</span>"); 3810 } 3811 metaBits.push("<span class='cv2-ctt-metaitem'><svg viewBox='0 0 24 24' width='15' height='15' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' aria-hidden='true'><path d='M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z'/><circle cx='12' cy='10' r='3'/></svg>" + cttLocalidade + "</span>"); 3812 if (cttPhone) { 3813 metaBits.push("<span class='cv2-ctt-metaitem'><svg viewBox='0 0 24 24' width='15' height='15' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' aria-hidden='true'><path d='M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3-8.7A2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.13.96.36 1.9.7 2.8a2 2 0 0 1-.45 2.1L8.1 9.9a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.45c.9.34 1.84.57 2.8.7A2 2 0 0 1 22 16.9Z'/></svg>" + cttPhone + "</span>"); 3814 } 3815 3816 $("#api-selected-html").html( 3817 "<h3 class='cv2-ctt-name'>" + formattedPointName + "</h3>" + 3818 "<div class='cv2-ctt-addr'>
3818" + formattedStreetName + "<br>" + cttLocalidade + "</div>" + 3819 "<div class='cv2-ctt-meta'>" + metaBits.join("<span class='cv2-ctt-metasep'></span>") + "</div>" 3820 ); 3821 // CTT tem as caixas de info/OBS estaticas no novo layout; so o worten injeta texto aqui 3822 $("#shopcart-api-txt").html(String(tipo) === "2" ? "" : "<br><span class='api_txt'>" + gl("info_worten") + "</span>"); // tipo chega como string 3823 $("#delivery_api_output").show(); 3824 3825 // Restore: substituir o texto provisorio da option pelo nome real do ponto 3826 var $cttOpt = $("#api_points option[value='" + selectedPaymentMethod + "']"); 3827 if ($cttOpt.length && $cttOpt.text() === "CTT") { $cttOpt.text(formattedPointName); $("#api_points").trigger("change.select2"); } 3828 3829 if (latitude || longitude) { 3830 $("#api-selected-address").show(); 3831 initMap(latitude, longitude, formattedPointName, selectedPaymentMethod); 3832 } else { 3833 initMapByAddress(formattedStreetName, town, formattedPointName); 3834 } 3835 3836 check_shopcart(); 3837 // portes dependem da zona do ponto CTT â refrescar o resumo (aside/sidebar) 3838 if (typeof populate_shopcart === "function") populate_shopcart(); 3839 $("#api_points_container").removeClass("LV_invalid_input"); 3840 } 3841 }); 3842 } 3843 3844 function initMapByAddress(address, city, title) { 3845 if (!address || !city) return; 3846 var fullAddress = address + ", " + city; 3847 var geocoder = new google.maps.Geocoder(); 3848 geocoder.geocode({ address: fullAddress }, function (results, status) { 3849 if (status !== "OK") { console.error("Geocode failed:", status); return; } 3850 var loc = results[0].geometry.location; 3851 var myLatLng = { lat: loc.lat(), lng: loc.lng() }; 3852 var map = new google.maps.Map(document.getElementById("api-selected-address"), { center: myLatLng, scrollwheel: false, zoom: 16 }); 3853 var marker = new google.maps.Marker({ 3854 map: map, position: myLatLng, title: title, 3855 url: "https://www.google.com/maps/dir/Current+Location/" + myLatLng.lat + "," + myLatLng.lng 3856 }); 3857 google.maps.event.addListener(marker, "click", function () { window.open(this.url, "_blank"); }); 3858 }); 3859 } 3860 3861 function initMap(e, t, n) { 3862 if (!t) return; 3863 var myLatLng = { lat: parseFloat(e), lng: parseFloat(t) }; 3864 var map = new google.maps.Map(document.getElementById("api-selected-address"), { center: myLatLng, scrollwheel: false, zoom: 16 }); 3865 var marker = new google.maps.Marker({ 3866 map: map, position: myLatLng, title: n, 3867 url: "https://www.google.com/maps/dir/Current+Location/" + e + "," + t 3868 }); 3869 google.maps.event.addListener(marker, "click", function () { window.open(this.url, "_blank"); }); 3870 // o container acabou de ser mostrado â sem resize o mapa fica cinzento (medido a 0x0) 3871 window.setTimeout(function () { google.maps.event.trigger(map, "resize"); map.setCenter(myLatLng); }, 400); 3872 } 3873 3874 function checkSelectedDeliveryMethod() { 3875 var e = 0; $(".deliverymethod").each(function () { if ($(this).hasClass("shopcart_delivery_button_selected")) e = $(this).attr("id"); }); return e; 3876 } 3877 function checkSelectedPaymentMethod() { 3878 var e = 0; $(".paymethod").each(function () { if ($(this).hasClass("shopcart_paytype_button_selected")) e = $(this).attr("id"); }); return e; 3879 } 3880 3881 // ===== gerar cesto completo (página /cart/) ===== 3882 function generate_shopcart(type, discountOverride) { 3883 var discount_code = typeof discountOverride !== "undefined" 3884 ? discountOverride 3885 : (($(".checkout-modern__cart-aside #discount_code").first().val() || $("#discount_code").last().val()) || ""); 3886 var t = type || "html"; 3887 3888 $.ajax({ 3889 type: "POST", 3890 url: REQUEST_ENDPOINT, 3891 cache: false, 3892 async: false, // manter compat com o antigo 3893 data: { 3894 ajax: "generate_shopcart", 3895 type: t, 3896 discount_code: discount_code 3897 }, 3898 success: function (html) { 3899 // injetar HTML 3900 $("#shopcart_html").html(html); 3901 3902 // lazy das imagens do cesto 3903 if ($.fn.lazy) { 3904 $(".lazy").lazy({ 3905 bind: "event", 3906 effect: "fadeIn", 3907 effectTime: 500, 3908 threshold: 0 3909 }); 3910 } 3911
3912 // se havia código, validar quantidades 3913 if (discount_code) { 3914 if (typeof validate_discount_code_qtd === "function") { 3915 validate_discount_code_qtd(discount_code); 3916 } 3917 } 3918 3919 // blocos especiais do teu legado 3920 if ($("#google_first").val() === "1") { 3921 $("#shopcart_option_buttons").hide(); 3922 $("#shopcart_login").show(); 3923 $("#delivery_methods").fadeIn(500); 3924 $("#shop_cart_table_form").fadeIn(500); 3925 $("#payment_methods").fadeIn(500); 3926 $("#comments_table").fadeIn(500); 3927 $("#gift_option_table").fadeIn(500); 3928 $("#gift_message_table").fadeIn(500); 3929 $("#optionals_table").fadeIn(500); 3930 $("#shop_cart_footer").fadeIn(500); 3931 $("#shop_cart_footer_info").fadeIn(500); 3932 $("#shop_cart_footer_iva").fadeIn(500); 3933 3934 if (typeof generate_cupidovip === "function") { 3935 generate_cupidovip($("#uid").val()); 3936 } 3937 } 3938 3939 // depois de gerar o cesto, aplica serviços especiais 3940 if (typeof shopcart_specialservices === "function") { 3941 shopcart_specialservices(); 3942 } 3943 3944 if (typeof initFancybox === "function") { 3945 initFancybox(); 3946 } 3947 } 3948 }); 3949 } 3950 3951 // Checkout page: keep the summary/products/totals in sync with drawer 3952 // mutations. Same ajax the qty handler uses (generate_shopcart + populate). 3953 // Do NOT call check_shopcart() here: its empty path fades out the form. 3954 function refreshCheckoutAfterCartChange() { 3955 if (!$(".checkout-modern").length || !$("#shopcart_html").length) return; 3956 if ($(".checkout-modern.cv2-confirmed").length || $("#order_info").is(":visible")) return; 3957 if (window.__cv2Leaving) return; 3958 if (window.__cv2CheckoutSyncing) return; 3959 window.__cv2CheckoutSyncing = true; 3960 var reloadType = $("#delivery_methods").is(":visible") ? "shopcart_remove" : "html"; 3961 if (typeof generate_shopcart === "function") generate_shopcart(reloadType); 3962 if (typeof populate_shopcart === "function") populate_shopcart(); 3963 if (typeof window.refreshCheckoutProductsCard === "function") { 3964 window.refreshCheckoutProductsCard({ refreshLists: false }); 3965 } 3966 window.setTimeout(function () { window.__cv2CheckoutSyncing = false; }, 800); 3967 } 3968 3969 // ===== gerar só o bloco de descontos do cesto ===== 3970 function generate_shopcart_discounts(code) { 3971 $.ajax({ 3972 type: "POST", 3973 url: REQUEST_ENDPOINT, 3974 cache: false, 3975 data: { 3976 ajax: "generate_shopcart_discounts", 3977 discount_code: code || "" 3978 }, 3979 success: function (html) { 3980 // este ID tem de existir no HTML do cesto 3981 $("#shopcart_discount_html").html(html); 3982 3983 // se usas o Floatl (antigo) para o label flutuante 3984 if (typeof Floatl !== "undefined") { 3985 new Floatl($("#discount_code").parent()); 3986 } 3987 } 3988 }); 3989 } 3990 3991 var cv2PopulateSeq = 0; 3992 function populate_shopcart(context) { 3993 var ctx = context || ""; 3994 // guard contra corridas: so a resposta do pedido MAIS RECENTE e aplicada 3995 // (mudar de pais disparava varios populates e a resposta antiga sobrescrevia os portes) 3996 var cv2Seq = ++cv2PopulateSeq; 3997 var userId = parseInt($("#uid").val(), 10) || 0; // "0" (string) e truthy â guests eram tratados como logados 3998 var selectedCountry = $("#country").val(); 3999 var userCountry = $("#user_country").val(); 4000 // t1644: ficha de envio manda; user_country (conta/sessao/encomenda anterior) so fallback 4001 if (!selectedCountry && userCountry) selectedCountry = userCountry; 4002 if (selectedCountry) $("#user_country").val(selectedCountry); 4003 4004 var deliveryCountry = $("#delivery_country").val(); 4005 var selectedDeliveryMethod = checkSelectedDeliveryMethod(); 4006 var selectedPaymentMethod = checkSelectedPaymentMethod(); 4007 var discountCode = ($(".checkout-modern__cart-aside #discount_code").first().val() || $("#discount_code").last().val()) || ""; 4008 var specialService = $("#special_service").val(); 4009 var sendAsGiftValue = $("#send_as_gift_value").val(); 4010 var sendAsGift = $('input[name="send_as_gift"]').is(':checked') ? "on" : ""; 4011 var api_points = (selectedDeliveryMethod == "sc_delivery_ctt" || selectedDeliveryMethod == "sc_delivery_worten") ? $("#api_points").val() : 0; 4012 4013 // garantir que o método CTT aparece (comportamento legado) 4014 $("#sc_delivery_ctt").parent().parent().show(); 4015 4016 $.ajax({ 4017 type: "POST", async: true, cache: false, url: REQUEST_ENDPOINT, 4018 data: { 4019 ajax: "populate_shopcart", 4020 uid: userId, country: selectedCountry, delivery_country: deliveryCountry, 4021 delivery_method: selectedDeliveryMethod, payment_method: selectedPaymentMethod, 4022 special_service: specialService, discount_code: discountCode, 4023 send_as_gift_value: sendAsGiftValue, send_as_gift: sendAsGift, 4024 // CV3 (2026-08-12): embrulhos multi â modo/linhas/dedicatória 4025 gift_wrap_enabled: $("#gift_wrap_enabled").val() || "0", 4026 gift_wrap_mode: $("#gift_wrap_mode").val() || "general", 4027 gift_wrap_lines: $("#gift_wrap_lines").val() || "", 4028 gift_wrap_dedicatoria: $("#gift_wrap_dedicatoria").val() || "", 4029 api_points: api_points 4030 }, 4031 success: function (response) { 4032 if (!response) return; 4033 if (cv2Seq !== cv2PopulateSeq) return; // resposta obsoleta (chegou outra depois) 4034 var cartData = {}; 4035 if (typeof response === "string") { 4036 try { cartData = JSON.parse(response); } 4037 catch (err) { cartData = {}; } 4038 } else if (typeof response === "object") { 4039 cartData = response || {}; 4040 } 4041 4042 if ($(".checkout-modern__layout--cart-page").length) { 4043 var modernItemCount = parseInt(cartData.all_total_items || cartData.total_items || cartData.items || 0, 10); 4044 var modernHasItems = !isNaN(modernItemCount) && modernItemCount > 0; 4045 $("body").toggleClass("checkout-modern-cart-empty", !modernHasItems); 4046 if (!modernHasItems) {
4047 $(".checkout-modern__cart-aside #shop_cart_footer, .checkout-modern__cart-aside #shop_cart_footer_info, .checkout-modern__promo, .checkout-modern__cart-checkout").hide(); 4048 } else { 4049 $(".checkout-modern__cart-aside").show(); 4050 $(".checkout-modern__cart-aside #shop_cart_footer, .checkout-modern__cart-aside #shop_cart_footer_info, .checkout-modern__promo, .checkout-modern__cart-checkout").show(); 4051 } 4052 var modernNoDiscount = cartData.subtotal_nodiscount_cambio || cartData.subtotal_nodiscount || "0.00"; 4053 var modernDiscount = cartData.discount_cambio || cartData.discount || "0.00"; 4054 var modernTotal = cartData.subtotal_cambio || cartData.subtotal || cartData.bigtotal_cambio || cartData.bigtotal || "0.00"; 4055 var modernHasDiscount = modernDiscount && modernDiscount !== "0" && modernDiscount !== "0.00" && $.trim(discountCode || "") !== ""; 4056 $(".checkout-modern-total-no-discount").text(modernNoDiscount); 4057 $(".checkout-modern-discount-value").text(modernDiscount); 4058 $(".checkout-modern-total-value").text(modernTotal); 4059 $(".checkout-modern__promo #apply_discount") 4060 .val(modernHasDiscount ? "REMOVER" : "APLICAR") 4061 .toggleClass("is-removing-discount", modernHasDiscount) 4062 .data("appliedCode", modernHasDiscount ? discountCode : ""); 4063 } 4064 4065 4066 updateHeaderCart(cartData); 4067 4068 function toggleRow(id, on) { 4069 // cv3: o proprio <li> e o metodo (a lista nova nao tem wrappers .pure-*; 4070 // o parent().parent() antigo escondia a LISTA INTEIRA â USA mostrava PayPal) 4071 var $m = $("#" + id); 4072 var $el = $m.is("li") ? $m : $m.parent().parent(); 4073 if (on) $el.stop(true, true).fadeIn(150); 4074 else $el.stop(true, true).hide(); 4075 } 4076 function fallbackPayment() { 4077 // selecionado ficou indisponivel p/ este pais: passar ao 1o metodo visivel 4078 var $first = $("#payment_methods .paymethod:visible").first(); 4079 if ($first.length && !$first.hasClass("shopcart_paytype_button_selected")) $first.trigger("click"); 4080 } 4081 4082 toggleRow("sc_payment_reception", cartData.cobranca_status !== "off"); 4083 4084 [["sc_payment_transfer","sc_estado3_status"], 4085 ["sc_payment_western","sc_estado4_status"], 4086 ["sc_payment_api_paypal","sc_estado5_status"], 4087 ["sc_payment_sibs_card","sc_estado6_status"], 4088 ["sc_payment_sibs_mb","sc_estado7_status"], 4089 ["sc_payment_sibs_mbway","sc_estado8_status"]].forEach(function (pair) { 4090 var id = pair[0], key = pair[1], isOn = cartData[key] !== "off"; 4091 toggleRow(id, isOn); 4092 }); 4093 // wallets acompanham o estado do paypal (mesmo carril) 4094 toggleRow("sc_payment_api_gpay", cartData.sc_estado5_status !== "off" && $("#sc_payment_api_gpay").hasClass("cv2-wallet-ready")); 4095 toggleRow("sc_payment_api_apple", cartData.sc_estado5_status !== "off" && $("#sc_payment_api_apple").hasClass("cv2-wallet-ready")); 4096 if ($("#" + selectedPaymentMethod).length && !$("#" + selectedPaymentMethod).is(":visible")) fallbackPayment(); 4097 if (cartData.cobranca_status === "off" && selectedPaymentMethod === "sc_payment_reception") fallbackPayment(); 4098 4099 selectedPaymentMethod = checkSelectedPaymentMethod(); 4100 4101 var discountValue = cartData.discount_cambio || cartData.discount; 4102 var hasDiscount = discountValue && discountValue !== "0" && discountValue !== "0.00"; 4103 var $discountBlocks = $("#discount_container_left, #discount_container_right"); 4104 if (hasDiscount) { 4105 $discountBlocks.stop(true, true).fadeIn(500); 4106 $("#sc_discount").html(discountValue); 4107 if (cartData.discount_percent) { 4108 $("#sc_discount_percent").html("(" + cartData.discount_percent + "%)"); 4109 } 4110 } else { 4111 $discountBlocks.stop(true, true).fadeOut(500); 4112 $("#sc_discount, #sc_discount_percent").empty(); 4113 } 4114 4115 [ 4116 ["#sc_subtotal", cartData.subtotal_cambio || cartData.subtotal], 4117 ["#sc_cobranca", cartData.cobranca_cambio || cartData.cobranca], 4118 ["#sc_shipping", cartData.shipping_cambio || cartData.shipping], 4119 ["#sc_weight", cartData.weight ? cartData.weight + " gr" : ""], 4120 ["#sc_total", cartData.subtotal_nodiscount_cambio || cartData.subtotal_nodiscount], 4121 ["#sc_bigtotal", cartData.bigtotal_cambio || cartData.bigtotal], 4122 ["#sc_optionals", cartData.sc_optionals], 4123 ["#sc_bigtotal_euros", cartData.bigtotal], 4124 ["#sc_iva", cartData.sc_iva], 4125 ["#sc_subtotal_comiva", cartData.sc_subtotal_comiva], 4126 ["#sc_iva_incidencia_23", cartData.sc_iva_incidencia_23], 4127 ["#sc_iva_total_23", cartData.sc_iva_total_23], 4128 ["#sc_iva_incidencia_6", cartData.sc_iva_incidencia_6], 4129 ["#sc_iva_total_6", cartData.sc_iva_total_6] 4130 ].forEach(function (entry) { 4131 if (typeof entry[1] !== "undefined" && entry[1] !== null) { 4132 $(entry[0]).html(entry[1]); 4133 } 4134 }); 4135 4136 // CV3 (2026-08-12): embrulhos de presente â render a partir dos dados
4137 // autoritativos do servidor (preço/linhas/modo). checkout-modern.js expõe 4138 // cv3RenderGiftwrap; se não existir (outra página), ignora sem erro. 4139 if (cartData.giftwrap && typeof window.cv3RenderGiftwrap === "function") { 4140 window.cv3RenderGiftwrap(cartData.giftwrap); 4141 } 4142 4143 $("#sc_information_text").html(cartData.obs || ""); 4144 if (cartData.obs) $("#sc_information_text_container").stop(true, true).fadeIn(200); 4145 else $("#sc_information_text_container").hide(); 4146 4147 // t1644: nao repor pais da sessao por cima da ficha 4148 var _fc = $("#country").val(); 4149 if (_fc) $("#user_country").val(_fc); 4150 else if (cartData.user_country) $("#user_country").val(cartData.user_country); 4151 if (typeof cartData.email_notes !== "undefined") $("#email_notes").val(cartData.email_notes); 4152 4153 var totalItems = parseInt(cartData.all_total_items || cartData.total_items || cartData.items || 0, 10); 4154 if (!isNaN(totalItems)) { 4155 var label = totalItems === 1 ? gl("ITEM") : gl("ITEMS"); 4156 $(".basketId").text(totalItems + " " + label); 4157 $(".basketIdMobile").text(totalItems); 4158 } 4159 4160 if (cartData.subtotal_cambio) { 4161 var currency = ($("#session_currency").val() || "").trim(); 4162 var subtotalText = currency ? currency + " " + cartData.subtotal_cambio : cartData.subtotal_cambio; 4163 $(".basketTotalId").text(subtotalText); 4164 $(".basketTotalIdMobile").text(subtotalText); 4165 } 4166 4167 if (ctx === "shopcart_initial_load" && typeof toastr !== "undefined" && Number(cartData.total_items || 0) >= 1) { 4168 if (Number(cartData.bigtotal_cambio || 0) <= Number(cartData.freeshipping_pt_cambio || 0)) { 4169 toastr.info(gl("CESTO_CONTINUAR")); 4170 } 4171 toastr.info(gl("TXT_TITLE_FINALIZAR_ENCOMENDA")); 4172 } 4173 } 4174 }); 4175 } 4176 4177 // ===== Inicializar Fancybox em todas as imagens ===== 4178 function initFancybox() { 4179 if (typeof $.fancybox === "undefined") return; 4180 4181 // links que são iframe 4182 var $iframeLinks = $( 4183 "a.fancybox\\.iframe, " + 4184 "a.fancybox[data-fancybox-type='iframe'], " + 4185 "a.fancybox[data-fb='iframe']" 4186 ); 4187 4188 $iframeLinks.fancybox({ 4189 type: 'iframe', 4190 smallBtn: true, 4191 iframe: { 4192 css: { 4193 width: '500px', 4194 height: '450px', 4195 maxWidth: '100%', 4196 maxHeight: '100%' 4197 } 4198 } 4199 }); 4200 4201 // resto das fancybox normais 4202 $(".fancybox") 4203 .not($iframeLinks) 4204 .fancybox(); 4205} 4206 4207$(function () { 4208 initFancybox(); 4209}); 4210 4211 4212 /* ========================================================== 4213 * 9.1) ENCOMENDA (SUBMIT) 4214 * ========================================================== */ 4215 function setSubmitLoading(isLoading) { 4216 var $btn = $("#shopcart_submit"); 4217 if (!$btn.length) return; 4218 if (isLoading) { 4219 if (!$btn.data("orig-html")) { 4220 $btn.data("orig-html", $btn.html()); 4221 } 4222 // estado visivel por tipo de metodo: pagamento imediato = "a iniciar o 4223 // pagamento"; assincrono (ref. MB/transferencia/cobranca) = "a registar" 4224 var cv2PayNow = { sc_payment_sibs_card: 1, sc_payment_sibs_mbway: 1, sc_payment_api_paypal: 1 }; 4225 var cv2M = checkSelectedPaymentMethod(); 4226 var cv2Txt = cv2PayNow[cv2M] 4227 ? (gl("CV2_CTA_STARTING") || "A iniciar o pagamento") 4228 : (gl("CV2_CTA_PLACING") || "A registar a encomenda"); 4229 $btn.prop("disabled", true).attr("aria-busy", "true") 4230 .html('<i class="fa fa-cog fa-spin" style="color:inherit;"></i> <span class="cv2-cta-status">' + cv2Txt + '</span>'); 4231 } else { 4232 var original = $btn.data("orig-html"); 4233 if (original) $btn.html(original); 4234 $btn.prop("disabled", false).removeAttr("aria-busy"); 4235 } 4236 } 4237 4238 function gatherShopcartRequestPayload() { 4239 var uid = parseInt($("#uid").val(), 10) || 0; 4240 var country = $("#country").val(); 4241 var userCountry = $("#user_country").val(); 4242 // t1644: ficha manda 4243 if (!country && userCountry) country = userCountry; 4244 if (country) $("#user_country").val(country); 4245 4246 var deliveryMethod = checkSelectedDeliveryMethod(); 4247 var sendGift = $('input[name="send_as_gift"]').is(':checked'); 4248 4249 return { 4250 ajax: "populate_shopcart", 4251 uid: uid, 4252 country: country, 4253 delivery_country: $("#delivery_country").val(), 4254 delivery_method: deliveryMethod, 4255 payment_method: checkSelectedPaymentMethod(), 4256 special_service: $("#special_service").val(), 4257 discount_code: $("#discount_code").val(), 4258 send_as_gift_value: $("#send_as_gift_value").val(), 4259 send_as_gift: sendGift ? "on" : "", 4260 // CV3 (2026-08-12): embrulhos multi (modo + linhas selecionadas + dedicatória) 4261 gift_wrap_enabled: $("#gift_wrap_enabled").val() || "0", 4262 gift_wrap_mode: $("#gift_wrap_mode").val() || "general", 4263 gift_wrap_lines: $("#gift_wrap_lines").val() || "", 4264 gift_wrap_dedicatoria: $("#gift_wrap_dedicatoria").val() || "", 4265 api_points: (deliveryMethod === "sc_delivery_ctt" || deliveryMethod === "sc_delivery_worten") ? $("#api_points").val() : 0 4266 }; 4267 } 4268 4269 function gatherOrderFormData() { 4270 var data = {}; 4271 data.email = $("#email").val() || $("#cacc_email").val(); 4272 data.password = $("#password").val(); 4273 data.nome = $("#nome").val() || $("#cacc_nome").val(); 4274 data.fiscal_id = $("#fiscal_id").val() || $("#cacc_fiscal").val(); 4275 data.morada = $("#morada").val() || $("#cacc_morada").val(); 4276 data.localidade = $("#localidade").val() || $("#cacc_localidade").val(); 4277 data.provincia = $("#provincia").val() || $("input[name='provincia']").val(); 4278 data.codigo_postal = $("#codigo_postal").val() || $("#cacc_cp").val(); 4279 data.country = $("#country").val() || $("#cacc_country").val(); 4280 data.telefone = $("#telefone").val() || $("#cacc_telefone").val(); 4281 data.sexo = $("#sexo").val(); 4282 data.newsletter = $("#newsletter").val(); 4283 data.birthday = $("#birthday").val(); 4284 data.newsletter_swing = $("#newsletter_swing").val(); 4285 data.delivery_nome = $("#delivery_nome").val(); 4286 data.delivery_morada = $("#delivery_morada").val(); 4287 data.delivery_localidade = $("#delivery_localidade").val(); 4288 data.delivery_provincia = $("#delivery_provincia").val() || ""; 4289 data.delivery_codigo_postal = $("#delivery_codigo_postal").val(); 4290 data.delivery_country = $("#delivery_country").val(); 4291 data.google_first = $("#google_first").val(); 4292 data.cv2_create_account = $("#cv2_create_account").is(":checked") ? 1 : ""; 4293 return data; 4294 } 4295 4296 // Idempotência (2026-08-15): chave imprevisÃvel por TENTATIVA LÃGICA de 4297 // criação da encomenda. Mantém-se em retry de rede (o servidor faz replay da 4298 // resposta original â nunca cria segunda encomenda); renova-se apenas quando 4299 // o servidor devolve uma falha de negócio (a tentativa seguinte é nova). 4300 function cv2IdemKey(reset) { 4301 if (reset || !window.__cv2IdemKey) { 4302 var hex = "";
4303 try { 4304 var a = new Uint8Array(32); 4305 (window.crypto || window.msCrypto).getRandomValues(a); 4306 for (var i = 0; i < a.length; i++) hex += ("0" + a[i].toString(16)).slice(-2); 4307 } catch (e) { 4308 hex = ""; 4309 for (var j = 0; j < 64; j++) hex += Math.floor(Math.random() * 16).toString(16); 4310 } 4311 window.__cv2IdemKey = hex; 4312 } 4313 return window.__cv2IdemKey; 4314 } 4315 4316 function gatherOrderData() { 4317 var data = {}; 4318 data.idempotency_key = cv2IdemKey(); 4319 // MBWAY: o telemóvel introduzido no checkout TEM de seguir para o servidor 4320 // (antes nunca era enviado e o backend usava o telefone da conta em bruto) 4321 data.mbway_phone = $.trim($("#sc_payment_sibs_mbway .checkout-modern__mbway-input").val() || ""); 4322 data.delivery_method = checkSelectedDeliveryMethod(); 4323 data.delivery_api_point = $("#api_points").val(); 4324 data.payment_method = checkSelectedPaymentMethod(); 4325 data.discount_code = $("#discount_code").val(); 4326 data.special_service_id = $("#special_service").val(); 4327 data.special_service_name = $("#special_service").val(); 4328 data.comments = $("#comments").val(); 4329 4330 if ($('input[name="send_as_gift"]').is(':checked')) { 4331 data.send_as_gift = 1; 4332 data.send_as_gift_value = $("#send_as_gift_value").val(); 4333 } 4334 4335 // CV3 (2026-08-12): embrulhos multi â modo/linhas/dedicatória (o preço é 4336 // recalculado no servidor a partir da configuração, nunca daqui). 4337 data.gift_wrap_enabled = $("#gift_wrap_enabled").val() || "0"; 4338 data.gift_wrap_mode = $("#gift_wrap_mode").val() || "general"; 4339 data.gift_wrap_lines = $("#gift_wrap_lines").val() || ""; 4340 data.gift_wrap_dedicatoria = $("#gift_wrap_dedicatoria").val() || ""; 4341 4342 return data; 4343 } 4344 4345 function fetchUTMContext() { 4346 try { 4347 return { 4348 utm_source: sessionStorage.getItem("utm_source") || "", 4349 utm_medium: sessionStorage.getItem("utm_medium") || "", 4350 utm_campaign: sessionStorage.getItem("utm_campaign") || "", 4351 referrer: sessionStorage.getItem("referrer") || "", 4352 landing_page: sessionStorage.getItem("landingPage") || "" 4353 }; 4354 } catch (err) { 4355 return { 4356 utm_source: "", 4357 utm_medium: "", 4358 utm_campaign: "", 4359 referrer: "", 4360 landing_page: "" 4361 }; 4362 } 4363 } 4364 4365 function handleOrderSuccess(rawResponse, orderData) { 4366 var response = {}; 4367 if (!rawResponse) return null; 4368 if (typeof rawResponse === "string" && rawResponse.trim() === "mail-err") { 4369 if (typeof swal === "function") { 4370 swal({ 4371 title: gl("swal_attention") || "Atenção", 4372 text: gl("EMAILNOK") || "O e-mail que introduziu parece não existir, por favor valide o campo E-mail.", 4373 type: "warning", 4374 confirmButtonColor: "#ABD559",
4375 showCancelButton: false, 4376 animation: false, 4377 confirmButtonText: gl("swal_btnok") || "OK", 4378 closeOnConfirm: true 4379 }); 4380 } 4381 return { error: true }; 4382 } 4383 if (typeof rawResponse === "string") { 4384 try { response = JSON.parse(rawResponse); } 4385 catch (err) { response = {}; } 4386 } else if (typeof rawResponse === "object") { 4387 response = rawResponse || {}; 4388 } 4389 4390 // Idempotência: a primeira execução ainda está a processar (duplo pedido 4391 // simultâneo) â informar e manter a MESMA chave (não é uma nova tentativa) 4392 if (response && response.error === "idem_processing") { 4393 if (typeof toastr !== "undefined") toastr.info(gl("CV2_IDEM_PROCESSING") || "A sua encomenda está a ser processada. Aguarde um instante por favor."); 4394 return { error: true, idemProcessing: true }; 4395 } 4396 4397 // CV2 Fase B5: conflito de stock â o gate atomico do processOrder recusou a compra 4398 // (rollBack, nenhum stock abatido, sem cobranca; em express a autorizacao e anulada). 4399 // Mostrar mensagem clara com o artigo e re-marcar o cesto (mantendo os restantes itens). 4400 if (response && response.error === "stock_conflict") { 4401 var scName = (response.name || "").toString(); 4402 var scTitle = gl("CV2_SC_TITLE") || "Artigo esgotado"; 4403 var scText = (gl("CV2_SC_TEXT") || "A última unidade de {N} esgotou entretanto. A compra não foi concluÃda.").replace("{N}", scName || "-"); 4404 if (typeof swal === "function") { 4405 swal({ title: scTitle, text: scText, type: "warning", confirmButtonColor: "#8e2447", showCancelButton: fal
4405se, animation: false, confirmButtonText: gl("swal_btnok") || "OK", closeOnConfirm: true }); 4406 } else if (typeof toastr !== "undefined") { 4407 toastr.error(scText); 4408 } 4409 // recomputar totais (o esgotado ja tem stock 0 -> excluido server-side) e re-marcar 4410 // o estado de stock no cesto/resumo (badge Esgotado + bloqueio do botao de compra) 4411 // t1570: regenerar HTML das linhas â senão o resumo fica com preços stale e totais a 0 4412 try { if (typeof window.generate_shopcart === "function") window.generate_shopcart("html"); } catch (e) {} 4413 try { if (typeof window.populate_shopcart === "function") window.populate_shopcart(); } catch (e) {} 4414 try { if (typeof window.cv2StockUxRefresh === "function") window.cv2StockUxRefresh(); } catch (e) {} 4415 return { error: true, stockConflict: true }; 4416 } 4417 4418 var general = response.general || {}; 4419 var paymentMethod = orderData.payment_method; 4420 4421 // Rotas reais (2026-08-14): metodos assincronos saem do /checkout/ para 4422 // /order-received/. IMPORTANTE (2026-08-15): sinalizar a saida ANTES de 4423 // navegar â sem isto o check_shopcart pos-encomenda esvazia o cesto e o 4424 // bootCv2EmptyWatch faz location.replace("/pt/") que CANCELA a navegacao 4425 // (causa real do "fui parar a homepage" no teste MBWAY do dono). 4426 var orderPageToken = general.order_token || ""; 4427 if (orderPageToken && (paymentMethod === "sc_payment_sibs_mb" || paymentMethod === "sc_payment_transfer" || paymentMethod === "sc_payment_reception")) { 4428 var opLang = $("#session_lang").val() || "pt"; 4429 window.__cv2Leaving = true; 4430 response.cv2Redirecting = true; 4431 window.location.href = "/" + opLang + "/order-received/?o=" + orderPageToken; 4432 return response; 4433 } 4434 4435 // MBWAY (2026-08-15): PERMANECE NO CHECKOUT â painel de espera integrado na 4436 // seccao de pagamento + polling autoritativo; aprovado -> /order-confirmed/; 4437 // recusado/cancelado/expirado -> mensagem + outro metodo (repay, MESMA 4438 // encomenda); desconhecido -> continuar a aguardar (nunca 2a cobranca). 4439 if (paymentMethod === "sc_payment_sibs_mbway" && orderPageToken) { 4440 var mbLang = $("#session_lang").val() || "pt"; 4441 // o legacy pode devolver a resposta MBWAY em sibs_mbway OU em sibs_card 4442 // (reutilizacao historica da variavel no servidor) 4443 var mbInitObj = response.sibs_mbway || response.sibs_card || null; 4444 var mbInitCode = (mbInitObj && mbInitObj.result && mbInitObj.result.code) ? String(mbInitObj.result.code) : ""; 4445 var mbInitOk = /^(000\.(000|200)\.|800\.400\.5)/.test(mbInitCode); 4446 if (typeof window.cv2MbwayCheckoutWait === "function") { 4447 window.__cv2Leaving = true; // suspende watchers de cesto vazio 4448 response.cv2Redirecting = true; // nao refrescar o cesto por cima do painel 4449 var mbPhone = $.trim($("#sc_payment_sibs_mbway .checkout-modern__mbway-input").val() || ""); 4450 window.cv2MbwayCheckoutWait({ 4451 token: orderPageToken, 4452 orderId: general.order_id || "", 4453 lang: mbLang, 4454 initOk: mbInitOk, 4455 initCode: mbInitCode, 4456 phone: mbPhone, 4457 repayUrl: "/" + mbLang + "/repay/" + (general.order_id || "") + "/" 4458 }); 4459 return response; 4460 } 4461 // fallback (modulo indisponivel): pagina order-received 4462 window.__cv2Leaving = true; 4463 response.cv2Redirecting = true; 4464 window.location.href = "/" + mbLang + "/order-received/?o=" + orderPageToken; 4465 return response; 4466 } 4467 4468 if (paymentMethod === "sc_payment_sibs_card" && response.sibs_card && response.sibs_card.id) { 4469 var sibsLang = $("#session_lang").val() || "pt"; 4470 // Fica no /checkout/ com COPYandPAY â sinalizar saida do fluxo de cesto 4471 // (como MBWAY) para o empty-watch nao mandar para a homepage quando o 4472 // processOrder esvazia NEWPROD. Refresh reabre o passo via shopcart.php. 4473 window.__cv2Leaving = true; 4474 response.cv2Redirecting = true; // nao regenerar cesto por cima do widget 4475 $.ajaxSetup({ cache: true }); 4476 window.wpwlOptions = { locale: sibsLang, paymentTarget: "_top", style: "plain" };
4477 $("#paymentform").attr("data-brands", "VISA VISAELECTRON VISADEBIT MASTER MASTERDEBIT MAESTRO AMEX"); 4478 // retorno do widget: handler autoritativo (valida na SIBS antes de decidir destino) 4479 if (orderPageToken) $("#paymentform").attr("action", "https://" + window.location.host + "/" + sibsLang + "/payment-return/?o=" + orderPageToken); 4480 $("#paymentinfo").html('<h1>' + (gl("SIBS_CARD") || "Pagamento com Cartão") + "</h1>"); 4481 $.getScript("https://eu-prod.oppwa.com/v1/paymentWidgets.js?checkoutId=" + response.sibs_card.id); 4482 } 4483 4484 if (paymentMethod === "sc_payment_sibs_mb") { 4485 // legacy: o cartao de confirmacao ja mostra Entidade/Referencia/Valor -> limpar o 4486 // titulo antigo do #paymentinfo que aparecia solto no topo ("Dados para pagamento MB") 4487 $("#paymentinfo").empty(); 4488 } 4489 4490 if (paymentMethod === "sc_payment_sibs_mbway") { 4491 // legacy: o cartao de confirmacao ja mostra o metodo (MB WAY) -> nao deixar o 4492 // titulo <h1>MB WAY</h1> solto no topo do __main (mesma logica do MB acima) 4493 $("#paymentinfo").empty(); 4494 } 4495 4496 if (paymentMethod === "sc_payment_api_paypal" && response.paypal) { 4497 $.ajax({ 4498 type: "POST", 4499 cache: false, 4500 url: REQUEST_ENDPOINT, 4501 data: { ajax: "paypal_soap", wsData: response.paypal }, 4502 success: function (data) { 4503 try { 4504 var parsed = typeof data === "string" ? JSON.parse(data) : data; 4505 if (parsed && parsed.redirectUrl) window.location.href = parsed.redirectUrl; 4506 } catch (err) { } 4507 } 4508 }); 4509 } 4510 4511 if (paymentMethod === "sc_payment_api" || paymentMethod === "sc_payment_api_paypal") { 4512 $(".hdrT1").hide(); 4513 } 4514 4515 $(window).scrollTop(0); 4516 $("#shopcart_login, #shopcart_html, #shopcart_cupidovip, #optionals_table, #encomenda, #payment_methods, #comments_table, #shop_cart_footer, #shop_cart_footer_info").hide(); 4517 $(".hdrT2, .hdrT3, #footer_container").hide(); 4518 $("#hipay_form_container").fadeIn(1200); 4519 4520 var successHtml = ""; 4521 if (general.html_success) successHtml += general.html_success; 4522 if (general.template_body) successHtml += general.template_body; 4523 4524 if (successHtml) { 4525 $("#order_info").html(successHtml).fadeIn(1200); 4526 } 4527 4528 var currency = ($("#session_currency").val() || "EUR"); 4529 $(".basketTotalId").text(currency + " 0.00"); 4530 $(".basketTotalIdMobile").text(currency + " 0.00"); 4531 $(".basketId").text("0 " + gl("ITEMS")); 4532 $(".basketIdMobile").text("0"); 4533 $("body").addClass("checkout-modern-cart-empty"); 4534 $(".checkout-modern__cart-aside").hide(); 4535 4536 return response; 4537 } 4538 4539 // CV2 (2026-09-21+): bloquear pagamento sem morada (home/alt) e/ou telemóvel 4540 function cv2GlOrFallback(key, fallback) { 4541 var v = (typeof gl === "function") ? gl(key) : ""; 4542 if (!v || v === key || v === "[" + key + "]") return fallback; 4543 return v; 4544 } 4545 function cv2CheckoutAddressMsg() { 4546 return cv2GlOrFallback( 4547 "CV2_SHIPPING_ADDRESS_REQUIRED", 4548 "Indique a morada de entrega completa (nome, morada, código postal, localidade e paÃs) antes de pagar." 4549 ); 4550 } 4551 function cv2CheckoutPhoneMsg() { 4552 return cv2GlOrFallback( 4553 "CV2_SHIPPING_PHONE_REQUIRED", 4554 "Indique um telemóvel válido (9 a 15 dÃgitos) antes de pagar." 4555 ); 4556 } 4557 function cv2PhoneOk(v) { 4558 var s = String(v == null ? "" : v).replace(/[\s\-().]/g, ""); 4559 if (s.charAt(0) === "+") s = s.slice(1); 4560 return /^[0-9]{9,15}$/.test(s); 4561 } 4562 function cv2CheckoutPhoneValue() { 4563 return $("#cacc_telefone").val() || $("#telefone").val() || $("#phone").val() || ""; 4564 } 4565 function cv2OpenAccountAddressForm(focusPhone) { 4566 if ($("#cacc_form").length) { 4567 $("#cacc_view").hide(); 4568 $("#cacc_form").show(); 4569 var $t = focusPhone ? $("#cacc_telefone") : $("#cacc_morada"); 4570 if (!$t.length) $t = $("#cacc_telefone").length ? $("#cacc_telefone") : $("#cacc_morada"); 4571 if ($t.length) { 4572 try { $("html, body").animate({ scrollTop: $t.offset().top - 110 }, 400); } catch (e1) {} 4573 $t.focus(); 4574 } 4575 } 4576 } 4577 function cv2CheckoutHasShippingAddress() { 4578 var dm = (typeof checkSelectedDeliveryMethod === "function") ? checkSelectedDeliveryMethod() : ""; 4579 function filled(v) { return $.trim(String(v == null ? "" : v)).length > 0; } 4580 // CTT/Worten: sem morada de rua; telemóvel de contacto (regra guest) 4581 if (dm === "sc_delivery_ctt" || dm === "sc_delivery_worten") { 4582 return cv2PhoneOk(cv2CheckoutPhoneValue()); 4583 } 4584 var addrOk = false;
4585 if (dm === "sc_delivery_alternative") { 4586 addrOk = filled($("#delivery_nome").val()) && filled($("#delivery_morada").val()) && 4587 filled($("#delivery_localidade").val()) && filled($("#delivery_codigo_postal").val()) && 4588 filled($("#delivery_country").val()); 4589 } else if ($("#cacc_morada").length) { 4590 addrOk = filled($("#cacc_nome").val()) && filled($("#cacc_morada").val()) && 4591 filled($("#cacc_localidade").val()) && filled($("#cacc_cp").val()) && 4592 filled($("#cacc_country").val()); 4593 } else { 4594 addrOk = filled($("#nome").val()) && filled($("#morada").val()) && 4595 filled($("#localidade").val()) && filled($("#codigo_postal").val()) && 4596 filled($("#country").val()); 4597 } 4598 return addrOk && cv2PhoneOk(cv2CheckoutPhoneValue()); 4599 } 4600 function cv2CheckoutMissingPhoneOnly() { 4601 var dm = (typeof checkSelectedDeliveryMethod === "function") ? checkSelectedDeliveryMethod() : ""; 4602 function filled(v) { return $.trim(String(v == null ? "" : v)).length > 0; } 4603 if (dm === "sc_delivery_ctt" || dm === "sc_delivery_worten") return !cv2PhoneOk(cv2CheckoutPhoneValue()); 4604 var addrOk = false; 4605 if (dm === "sc_delivery_alternative") { 4606 addrOk = filled($("#delivery_nome").val()) && filled($("#delivery_morada").val()) && 4607 filled($("#delivery_localidade").val()) && filled($("#delivery_codigo_postal").val()) && 4608 filled($("#delivery_country").val()); 4609 } else if ($("#cacc_morada").length) { 4610 addrOk = filled($("#cacc_nome").val()) && filled($("#cacc_morada").val()) && 4611 filled($("#cacc_localidade").val()) && filled($("#cacc_cp").val()) && 4612 filled($("#cacc_country").val()); 4613 } else { 4614 addrOk = filled($("#nome").val()) && filled($("#morada").val()) && 4615 filled($("#localidade").val()) && filled($("#codigo_postal").val()) && 4616 filled($("#country").val()); 4617 } 4618 return addrOk && !cv2PhoneOk(cv2CheckoutPhoneValue()); 4619 } 4620 4621 function submitOrder() { 4622 // espera MBWAY ativa: a encomenda JA existe e o pedido esta na app â nada 4623 // pode voltar a submeter (o disabled do botao nao chega: proxies com 4624 // .trigger('click') disparam handlers jQuery mesmo em botoes disabled, e 4625 // um populate_shopcart podia reativa-lo) 4626 if (window.__cv2MbwayWaitActive) return false; 4627 // remover o ultimo artigo do cesto NUNCA pode criar encomenda 4628 if (window.__cv2UserEmptiedCart || window.__cv2CartRemoving || $("#shopcart_empty").length) { 4629 if (typeof toastr !== "undefined") toastr.info(gl("CESTO_VAZIO1") || "O cesto esta vazio"); 4630 return false; 4631 } 4632 // Sample-only / sem produto normal (restauro check_amostras + CONFIGURAÃÃES) 4633 if ($("#check_amostras").length && (parseInt($("#check_amostras").val(), 10) || 0) < 1) { 4634 var cv2SamplesOnlyMsg = ($("#check_amostras").attr("data-samples-only") === "1") 4635 ? (gl("CV2_SAMPLES_ONLY_CHECKOUT") || "Não é possÃvel finalizar uma encomenda só com amostras. Adicione pelo menos um produto da loja.") 4636 : (gl("COMPRAS_A_ZERO") || "O seu carrinho está vazio."); 4637 if (typeof toastr !== "undefined") toastr.error(cv2SamplesOnlyMsg); 4638 else if (typeof Swal !== "undefined") Swal.fire({ text: cv2SamplesOnlyMsg, icon: "info" }); 4639 else alert(cv2SamplesOnlyMsg); 4640 return false; 4641 } 4642 if (!cv2CheckoutHasShippingAddress()) { 4643 var phoneOnly = (typeof cv2CheckoutMissingPhoneOnly === "function") && cv2CheckoutMissingPhoneOnly(); 4644 cv2OpenAccountAddressForm(!!phoneOnly);
4645 var msg = phoneOnly ? cv2CheckoutPhoneMsg() : cv2CheckoutAddressMsg(); 4646 if (typeof toastr !== "undefined") toastr.error(msg); 4647 else if (typeof swal === "function") { 4648 swal({ title: gl("swal_attention") || "Atenção", text: msg, type: "warning", confirmButtonColor: "#ABD559", confirmButtonText: gl("swal_btnok") || "OK" }); 4649 } else { alert(msg); } 4650 return false; 4651 } 4652 if (!$('input[name="terms"]').is(":checked")) { 4653 if (typeof swal === "function") { 4654 swal({ 4655 title: gl("swal_attention") || "Atenção", 4656 text: gl("swal_missing_fields") || "Tem de aceitar os termos.", 4657 type: "warning", 4658 confirmButtonColor: "#ABD559", 4659 showCancelButton: false, 4660 animation: false, 4661 confirmButtonText: gl("swal_btnok") || "OK", 4662 closeOnConfirm: true 4663 }); 4664 } else { 4665 alert(gl("swal_missing_fields") || "Tem de aceitar os termos."); 4666 } 4667 return false; 4668 } 4669 4670 setSubmitLoading(true); 4671 4672 var populatePayload = gatherShopcartRequestPayload(); 4673 4674 $.ajax({ 4675 type: "POST", 4676 cache: false, 4677 url: REQUEST_ENDPOINT, 4678 data: populatePayload 4679 }) 4680 .done(function (response) { 4681 var shopcartData = {}; 4682 if (typeof response === "string") { 4683 try { shopcartData = JSON.parse(response); } 4684 catch (err) { shopcartData = {}; } 4685 } else if (typeof response === "object") { 4686 shopcartData = response || {}; 4687 } 4688 4689 var formData = gatherOrderFormData(); 4690 var orderData = gatherOrderData(); 4691 var utmData = fetchUTMContext(); 4692 4693 $.ajax({ 4694 type: "POST", 4695 cache: false, 4696 url: REQUEST_ENDPOINT, 4697 data: $.extend({ 4698 ajax: "processOrder", 4699 orderData: orderData, 4700 formData: formData, 4701 shopcartData: shopcartData 4702 }, utmData) 4703 }) 4704 .done(function (result) { 4705 var parsed = handleOrderSuccess(result, orderData); 4706 if (!parsed || parsed.error) { 4707 setSubmitLoading(false); 4708 // falha de NEGÃCIO confirmada pelo servidor -> a próxima submissão é 4709 // uma nova tentativa lógica (chave nova). Em idem_processing a chave 4710 // mantém-se (a primeira execução ainda decorre). 4711 if (!parsed || parsed.idemProcessing !== true) cv2IdemKey(true); 4712 if (parsed && parsed.stockConflict) { 4713 // ja tratado em handleOrderSuccess (mensagem clara + refresh do cesto) 4714 } else if (typeof toastr !== "undefined") { 4715 if (parsed && (parsed.error === "shipping_address_required" || parsed.error === "shipping_phone_required")) { 4716 cv2OpenAccountAddressForm(parsed.error === "shipping_phone_required"); 4717 } 4718 if (parsed && parsed.message) { 4719 var cv2ErrMsg = parsed.message; 4720 if (parsed.error === "shipping_address_required" || cv2ErrMsg === "CV2_SHIPPING_ADDRESS_REQUIRED") { 4721 cv2ErrMsg = cv2CheckoutAddressMsg(); 4722 } else if (parsed.error === "shipping_phone_required" || cv2ErrMsg === "CV2_SHIPPING_PHONE_REQUIRED") { 4723 cv2ErrMsg = cv2CheckoutPhoneMsg(); 4724 } 4725 if (parsed.products && parsed.products.length) cv2ErrMsg += " " + parsed.products.join(", ") + "."; 4726 toastr.error(cv2ErrMsg); 4727 } else { 4728 toastr.error(gl("swal_missing_fields") || "Ocorreu um erro ao processar a encomenda."); 4729 } 4730 } 4731 } else { 4732 // em navegacao/espera pos-encomenda NAO refrescar o cesto: o refresh 4733 // esvazia a UI e os watchers de cesto-vazio mandavam para a homepage 4734 if (!parsed.cv2Redirecting) check_shopcart(); 4735 } 4736 }) 4737 .fail(function () { 4738 setSubmitLoading(false); 4739 if (typeof toastr !== "undefined") toastr.error("Não foi possÃvel processar a encomenda."); 4740 }); 4741 }) 4742 .fail(function () { 4743 setSubmitLoading(false); 4744 if (typeof toastr !== "undefined") toastr.error("Não foi possÃvel validar o carrinho."); 4745 }); 4746 4747 return false;
4748 } 4749 4750 /* t1407: telemóvel PT/intl â number:true rejeitava +351... */ 4751 function cv2EnsurePhoneValidator() { 4752 if (typeof jQuery === "undefined" || !jQuery.validator || jQuery.validator.methods.cv2phone) return; 4753 jQuery.validator.addMethod("cv2phone", function (value, element) { 4754 if (this.optional(element)) return true; 4755 var s = String(value || "").replace(/[\s\-().]/g, ""); 4756 if (s.charAt(0) === "+") s = s.slice(1); 4757 return /^[0-9]{9,15}$/.test(s); 4758 }, "Introduza um telemóvel válido."); 4759 } 4760 4761 function initOrderForm() { 4762 var $form = $("#encomenda"); 4763 if (!$form.length || typeof $.fn.validate === "undefined") return; 4764 cv2EnsurePhoneValidator(); 4765 4766 $form.off("submit.orderForm").on("submit.orderForm", function (e) { 4767 e.preventDefault(); 4768 }); 4769 4770 $("#shopcart_submit").off("click.order").on("click.order", function (e) { 4771 e.preventDefault(); 4772 if ($form.valid && $form.valid()) { 4773 submitOrder(); 4774 } else { 4775 $form.submit(); 4776 } 4777 }); 4778 4779 if ($form.data("validator")) return; 4780 4781 $form.validate({ 4782 ignore: ":not(:visible)", 4783 focusInvalid: false, 4784 rules: { 4785 delivery_nome: { required: true }, 4786 delivery_morada: { required: true }, 4787 delivery_localidade: { required: true }, 4788 delivery_codigo_postal: { required: true }, 4789 delivery_provincia: { required: true }, 4790 provincia: { required: true }, 4791 nome: { required: true }, 4792 email: { required: true, email: true }, 4793 email2: { required: true, equalTo: "#email" }, 4794 password: { required: true, minlength: 6 }, 4795 password2: { equalTo: "#password" }, 4796 morada: { required: true }, 4797 localidade: { required: true }, 4798 codigo_postal: { required: true }, 4799 telefone: { required: true, cv2phone: true }, 4800 api_points: { required: true } 4801 }, 4802 invalidHandler: function (event, validator) { 4803 /* t1589: sem popup â só highlight + scroll/foco no 1.º campo inválido */ 4804 if (validator && validator.errorList && validator.errorList.length) { 4805 var el = validator.errorList[0].element; 4806 $("html, body").animate({ scrollTop: $(el).offset().top - 110 }, 1000); 4807 $(el).focus().trigger("click"); 4808 } 4809 }, 4810 onkeyup: function (element) { 4811 $(element).removeClass("LV_invalid_input"); 4812 }, 4813 onfocusout: function (element) { 4814 var $el = $(element); 4815 if ($el.valid && $el.valid()) { 4816 if (element.id === "api_points") $("#api_points_container").removeClass("LV_invalid_input"); 4817 else $el.removeClass("LV_invalid_input"); 4818 } else { 4819 if (element.id === "api_points") { 4820 /* t1589: sem popup no focusout â só highlight + label */ 4821 $("#api_points_container").addClass("LV_invalid_input"); 4822 } else { 4823 $el.addClass("LV_invalid_input"); 4824 } 4825 } 4826 }, 4827 messages: (window.cv2Jqv && cv2Jqv.fieldMessages) ? cv2Jqv.fieldMessages() : {}, 4828 errorPlacement: function (error, element) { 4829 if (window.cv2Jqv && cv2Jqv.errorPlacement) return cv2Jqv.errorPlacement(error, element); 4830 error.insertAfter(element); 4831 }, 4832 showErrors: function (errorMap, errorList) { 4833 if (window.cv2Jqv && cv2Jqv.showErrorsWithLabels) { 4834 return cv2Jqv.showErrorsWithLabels.call(this, errorMap, errorList); 4835 } 4836 $.each(errorList, function (_, item) { 4837 $(item.element).addClass("LV_invalid_input"); 4838 }); 4839 this.defaultShowErrors(); 4840 }, 4841 submitHandler: function () { 4842 submitOrder(); 4843 } 4844 }); 4845 } 4846 4847 /* Quick Add + navegação URL legada (mantidos) */ 4848 $(function () { 4849 $("body").on("click.nav", ".webstore-nav-bottom", function () { 4850 $('html, body').animate({ scrollTop: $("#catalog_top").offset().top }, 1000); 4851 }); 4852 4853 $("body").on("click.qa", ".btnQuickAdd", function () { 4854 var elementos = $(this).attr("elementos"); 4855 var id_code = $(this).attr("id_code"); 4856 var furl = $(this).attr("furl"); 4857 var lang = $(this).attr("lang"); 4858 if (Number(elementos) > 0) { 4859 window.location.href = window.location.origin + '/' + lang + '/prod/' + furl + '/' + id_code + '/'; 4860 } else { 4861 $.post(REQUEST_ENDPOINT, { ajax: "shopcart_add", id_code: id_code }, function () { 4862 check_shopcart(); /* t1592: sem toast SHOPCART_ADDED_OK */ 4863 }); 4864 } 4865 }); 4866 }); 4867 4868 4869 /* ========================================================== 4870 * 10) CESTO NO TOPO 4871 * ========================================================== */ 4872 4873 function updateHeaderCart(data) { 4874 if (!data) { 4875 // pedido leve ao backend se não for passado o objeto 4876 return $.ajax({ 4877 type: "POST", 4878 url: REQUEST_ENDPOINT, 4879 data: { ajax: "populate_shopcart" }, 4880 dataType: "json", 4881 cache: false 4882 }).done(updateHeaderCart); 4883 } 4884 4885 var items = parseInt(data.all_total_items || data.total_items || data.items || 0, 10); 4886 var itemText = items + " " + (items === 1 ? gl("ITEM") : gl("ITEMS")); 4887 4888 // Desktop â com texto / Mobile â só número 4889 $(".basketId").text(itemText); 4890 $(".basketIdMobile").text(items); 4891 4892 var cur = ($("#session_currency").val() || data.currency || "EUR").toUpperCase(); 4893 var SYM = { EUR: "â¬", USD: "$", GBP: "£", BRL: "R$" }; 4894 var total = data.bigtotal_cambio || data.subtotal_cambio || data.bigtotal || data.subtotal || 0; 4895 var formatted = (SYM[cur] || cur + " ") + parseFloat(total || 0).toFixed(2).replace(".", ","); 4896 4897 $(".basketTotalId, .basketTotalIdMobile").text(formatted); 4898 } 4899 4900 /* ===== Check cart (header + mini-cart + legacy layout) ===== */ 4901 function check_shopcart(mode) { 4902 $.ajax({ 4903 type: "POST", 4904 url: REQUEST_ENDPOINT + "?ts=" + Date.now(), // cache buster 4905 data: { ajax: "check_shopcart" }, 4906 cache: false 4907 }) 4908 .done(function (response) { 4909 var dataObject = null; 4910 if (response && typeof response === "object") { 4911 dataObject = response; 4912 } else if (typeof response === "string") { 4913 try { dataObject = JSON.parse(response); } 4914 catch (err) { dataObject = null; } 4915 } 4916 4917 var raw = (typeof response === "string") ? response.trim() : response; 4918 var hasCart = Array.isArray(dataObject) ? dataObject.length > 0 : !!(raw && raw !== "null"); 4919 4920 // Atualiza cabeçalho com os dados recebidos; se não vierem, faz fallback 4921 if (dataObject) updateHeaderCart(dataObject); 4922 else updateHeaderCart(); 4923 4924 if (hasCart) { 4925 $("body").removeClass("checkout-modern-cart-empty");
4926 $(".checkout-modern__cart-aside").show(); 4927 // Fluxo legado quando estamos a iniciar o checkout completo 4928 if (mode === "init") { 4929 var uid = parseInt($("#uid").val(), 10) || 0; 4930 if (uid > 0 && typeof generate_cupidovip === "function") { 4931 generate_cupidovip(uid); 4932 $("#shopcart_cupidovip").stop(true, true).show(); 4933 } else { 4934 $("#shopcart_login").show(); 4935 } 4936 4937 var fadeInSelectors = [ 4938 "#delivery_methods", 4939 "#shop_cart_table_form", 4940 "#payment_methods", 4941 "#comments_table", 4942 "#gift_option_table", 4943 "#gift_message_table", 4944 "#optionals_table", 4945 "#shop_cart_footer", 4946 "#shop_cart_footer_info", 4947 "#shop_cart_footer_iva" 4948 ]; 4949 4950 fadeInSelectors.forEach(function (selector) { 4951 $(selector).stop(true, true).fadeIn(500); 4952 }); 4953 } 4954 4955 if (typeof populate_shopcart === "function") { 4956 populate_shopcart(); 4957 } 4958 4959 if (typeof shopcart_specialservices === "function") { 4960 shopcart_specialservices(); 4961 } 4962 4963 if (typeof generate_shopcart_modal === "function") { 4964 generate_shopcart_modal(); 4965 } 4966 } else { 4967 // t1683: on checkout-v2, do NOT fadeOut Conta/Entrega/Payment â 4968 // empty check_shopcart can race after sections were shown. 4969 var isCheckoutV2 = $(".checkout-modern.checkout-v2").length > 0; 4970 if (!isCheckoutV2) { 4971 var fadeOutSelectors = [ 4972 "#shopcart_cupidovip", 4973 "#shopcart_login", 4974 "#delivery_methods", 4975 "#shop_cart_table_form", 4976 "#payment_methods", 4977 "#comments_table", 4978 "#gift_option_table", 4979 "#gift_message_table", 4980 "#optionals_table", 4981 "#shop_cart_footer", 4982 "#shop_cart_footer_info", 4983 "#shop_cart_footer_iva" 4984 ]; 4985 4986 fadeOutSelectors.forEach(function (selector) { 4987 $(selector).stop(true, true).fadeOut(500); 4988 }); 4989 $(".checkout-modern__cart-aside").hide(); 4990 } 4991 4992 var currency = ($("#session_currency").val() || "EUR").toUpperCase(); 4993 $(".basketTotalId").text(currency + " 0.00"); 4994 $(".basketTotalIdMobile").text(currency + " 0.00"); 4995 $(".basketId").text("0 " + gl("ITEMS")); 4996 $(".basketIdMobile").text("0"); 4997 $("body").addClass("checkout-modern-cart-empty"); 4998 } 4999 5000 // Se o mini-cart estiver visÃvel, refresca o HTML 5001 var $mc = $("#shopcart-modal-container, #mini-cart"); 5002 if ($mc.is(":visible")) { 5003 $.post(REQUEST_ENDPOINT, { ajax: "generate_shopcart", type: "html" }) 5004 .done(function (html) { 5005 if (typeof window.renderShopcartDrawerHtml === "function") { 5006 window.renderShopcartDrawerHtml(html); 5007 } else if ($("#shopcart-modal").length) { 5008 $("#shopcart-modal").html(html); 5009 } else { 5010 $mc.html(html); 5011 } 5012 }); 5013 } 5014 }) 5015 .fail(function (xhr) { 5016 console.debug("check_shopcart FAIL", xhr.status, xhr.responseText); 5017 }); 5018 5019 if (mode === "init") { 5020 $("#footer_container").stop(true, true).fadeIn(1500); 5021 } 5022 } 5023 window.check_shopcart = check_shopcart; 5024 5025 /* ========================================================== 5026 * MINI-CART DRAWER 5027 * ========================================================== */ 5028 function bindMiniCartHover() { 5029 var $desktopPanel = $("#shopcart-modal-container"); 5030 if (!$desktopPanel.length) return; 5031 if (!$desktopPanel.parent().is("body")) { 5032 $desktopPanel.appendTo("body"); 5033 $desktopPanel = $("#shopcart-modal-container"); 5034 } 5035 5036 if (!$("#shopcart-drawer-overlay").length) { 5037 $("body").append("<div id=\"shopcart-drawer-overlay\" class=\"shopcart-drawer-overlay\"></div>"); 5038 } 5039 5040 var SHOPCART_SAVED_COLLAPSED_KEY = "cupido_shopcart_saved_for_later_collapsed"; 5041 5042 function currentPanel() { 5043 return $desktopPanel; 5044 } 5045 5046 function isSavedForLaterCollapsed() { 5047 try { return localStorage.getItem(SHOPCART_SAVED_COLLAPSED_KEY) === "1"; } catch (e) { return false; } 5048 } 5049 5050 function setSavedForLaterCollapsed(collapsed) { 5051 try { localStorage.setItem(SHOPCART_SAVED_COLLAPSED_KEY, collapsed ? "1" : "0"); } catch (e) {} 5052 } 5053 5054 function applySavedForLaterState($slot) { 5055 var $section = ($slot && $slot.length ? $slot : $currentSavedSlot()).find(".shopcart-drawer__saved-section"); 5056 if (!$section.length) return; 5057 var empty = currentPanel().find(".shopcart_empty, .shopcart-drawer__empty").length > 0; 5058 var savedCount = $section.find(".shopcart-drawer__saved-item").length; 5059 if (!savedCount) { 5060 var badge = parseInt($.trim($section.find(".shopcart-drawer__saved-head strong").first().text()), 10); 5061 if (!isNaN(badge)) savedCount = badge; 5062 } 5063 if (empty && savedCount > 0) { 5064 $section.removeClass("is-collapsed"); 5065 return; 5066 } 5067 $section.toggleClass("is-collapsed", empty || isSavedForLaterCollapsed()); 5068 } 5069 5070 function setShopcartDrawerLoading(active, $scope) { 5071 var $target = ($scope && $scope.length) ? $scope : currentPanel(); 5072 if (!$target || !$target.length) return; 5073 if (!$target.find(".shopcart-drawer__loader").length) { 5074 $target.append("<div class=\"shopcart-drawer__loader\" aria-hidden=\"true\"><span></span></div>"); 5075 } 5076 $target.toggleClass("is-shopcart-loading", !!active); 5077 $target.attr("aria-busy", active ? "true" : "false"); 5078 } 5079 window.setShopcartDrawerLoading = setShopcartDrawerLoading; 5080
5081 function drawerLabels() { 5082 var l = window.CUPIDO_CART_DRAWER_LABELS || {}; 5083 return { 5084 eyebrow: l.eyebrow || gl("SHOPCART_DRAWER_EYEBROW") || "Cesto de compras", 5085 title: l.title || gl("SHOPCART_DRAWER_TITLE") || (currentLangCode() === "en" ? "Bag" : currentLangCode() === "es" ? "Cesta" : "Cesto"), 5086 close: l.close || gl("SHOPCART_DRAWER_CLOSE") || "Fechar", 5087 subtotal: l.subtotal || gl("SHOPCART_DRAWER_SUBTOTAL") || "Subtotal", 5088 shipping: l.shipping || gl("SHOPCART_DRAWER_SHIPPING") || "Portes", 5089 shippingPending: l.shippingPending || gl("SHOPCART_DRAWER_SHIPPING_PENDING") || "A calcular", 5090 savings: l.savings || gl("SHOPCART_DRAWER_SAVINGS") || "Descontos", 5091 total: l.total || gl("SHOPCART_DRAWER_TOTAL") || "Total", 5092 checkout: l.checkout || gl("SHOPCART_DRAWER_CHECKOUT") || "CONTINUAR PARA O CHECKOUT", 5093 cart: l.cart || gl("SHOPCART_DRAWER_CART") || "IR PARA O CESTO", 5094 freeShippingFrom: l.freeShippingFrom || gl("SHOPCART_DRAWER_FREE_SHIPPING_FROM") || "Portes grátis em {N} a partir de", 5095 freeShippingRemaining: l.freeShippingRemaining || gl("SHOPCART_DRAWER_FREE_SHIPPING_REMAINING") || "faltam para portes grátis", 5096 freeShippingRemaining2: l.freeShippingRemaining2 || gl("SHOPCART_DRAWER_FS_REMAINING2") || "Faltam %s para portes grátis", 5097 freeShippingQualified: l.freeShippingQualified || gl("SHOPCART_DRAWER_FREE_SHIPPING_QUALIFIED") || "Já tem portes grátis para Portugal Continental", 5098 shippingFree: l.shippingFree || gl("SHOPCART_DRAWER_SHIPPING_FREE") || "Grátis", 5099 totalEstimated: l.totalEstimated || gl("SHOPCART_DRAWER_TOTAL_ESTIMATED") || "Total estimado", 5100 plusShipping: l.plusShipping || gl("SHOPCART_DRAWER_PLUS_SHIPPING") || "+ portes", 5101 expressNote: l.expressNote || gl("CV2_DX_NOTE") || "O valor final e os portes são confirmados após selecionar a morada.", 5102 promoQuestion: l.promoQuestion || gl("SHOPCART_DRAWER_PROMO_Q") || "Tem um código de desconto?", 5103 promoPlaceholder: l.promoPlaceholder || gl("SHOPCART_CODIGODESCONTO") || "Código de desconto", 5104 promoApply: l.promoApply || gl("CHECKOUT_APPLY_DISCOUNT_BUTTON") || "Aplicar", 5105 promoRemove: l.promoRemove || gl("CHECKOUT_SUMMARY_REMOVE_CODE") || "Remover", 5106 shippingCalc: l.shippingCalc || gl("SHOPCART_DRAWER_CALC") || "Calcular", 5107 expressTitle: l.expressTitle || gl("CV2_EXPRESS_TITLE") || "Express checkout", 5108 expressOr: l.expressOr || gl("CV2_EXPRESS_OR") || "ou continuar com checkout normal" 5109 }; 5110 } 5111 5112 function drawerCurrency() { 5113 return ($("#session_currency").val() || "â¬").trim() || "â¬"; 5114 } 5115 5116 function buildDrawerItems($content) { 5117 var rows = []; 5118 $content.find("tr").each(function () { 5119 if ($(this).find(".shopcart_remove").length) rows.push(this); 5120 }); 5121 5122 if (!rows.length) { 5123 return $content.find(".shopcart_empty, .shopcart_empty_modal").first().length ? $content.html() : ""; 5124 } 5125 5126 var items = []; 5127 $.each(rows, function (_, row) { 5128 var $td = $(row).children("td"); 5129 var $remove = $td.eq(0).find(".shopcart_remove").first().clone(); 5130 var $img = $td.eq(1).find("img").first().clone(); 5131 var $detail = $td.eq(2).clone(); 5132 var discountHtml = $.trim($td.eq(3).html() || ""); 5133 var $qty = $td.eq(4).find("select").first().clone(); 5134 var unitPrice = $.trim($td.eq(5).text() || ""); 5135 var lineTotal = $.trim($td.last().text() || ""); 5136 // layout PV: strike em cima; badge -% + preco por baixo (coluna a direita do titulo) 5137 var $dxDisc = $("<div></div>").html(discountHtml); 5138 var dxStrike = $dxDisc.find("strike").length ? "<span class=\"pvx-strike\">" + $.trim($dxDisc.find("strike").first().text()) + "</span>" : ""; 5139 $dxDisc.find("strike").remove(); 5140 var dxPct = $.trim($dxDisc.text()); 5141 var pricesPart = "<span class=\"shopcart-drawer__prices\">" + dxStrike + 5142 "<span class=\"pvx-now\">" + (dxPct ? "<span class=\"pvx-badge\">" + dxPct + "</span>" : "") + 5143 (unitPrice ? "<span class=\"pvx-price\">" + unitPrice + "</span>" : "") + "</span></span>"; 5144 var totalPart = lineTotal ? "<strong class=\"shopcart-drawer__price-total\">" + lineTotal + "</strong>" : ""; 5145 // linha oferta (total 0 com unitario > 0): badge OFERTA, sem precos 5146 var dxGiftT = (lineTotal || "").match(/([0-9]+)[.,]([0-9]{2})/); 5147 var dxGiftU = (unitPrice || "").match(/([0-9]+)[.,]([0-9]{2})/); 5148 if (dxGiftT && parseFloat(dxGiftT[1] + "." + dxGiftT[2]) === 0 && dxGiftU && parseFloat(dxGiftU[1] + "." + dxGiftU[2]) > 0) { 5149 var dxGiftLbl = "OFERTA"; 5150 if (window.gl) { var dxGiftG = window.gl("CV2_OFFER_BADGE"); if (dxGiftG && dxGiftG !== "CV2_OFFER_BADGE") dxGiftLbl = dxGiftG; } 5151 pricesPart = "<span class=\"shopcart-drawer__prices\"><span class=\"pvx-now\"><span class=\"pvx-badge\">" + dxGiftLbl + "</span></span></span>"; 5152 totalPart = ""; 5153 } 5154 5155 var $dxSub = $detail.find(".product_subinfo").first(); 5156 var variantText = ""; 5157 if ($dxSub.length) { 5158 // cor/tamanho e o ultimo segmento do subinfo (formatos com Ref/CB antes de <br>) 5159 var dxSubParts = ($dxSub.html() || "").split(/<br\s*\/?>/i); 5160 variantText = $.trim($("<div></div>").html(dxSubParts[dxSubParts.length - 1]).text()); 5161 if (/^Ref:/i.test(variantText) || /CB:/i.test(variantText)) variantText = ""; 5162 } 5163 $detail.find("br").replaceWith(" "); 5164 $detail.find(".product_subinfo").remove(); 5165 $detail.find("a").removeAttr("style"); 5166 $qty.addClass("shopcart-drawer__qty-select"); 5167 $remove.addClass("shopcart-drawer__remove").removeClass("fa-minus-circle").addClass("fa-trash"); 5168 if ($remove.is("button") && !$remove.attr("type")) $remove.attr("type", "button"); 5169 5170 // stepper como no checkout: lixo a qty=1, +/- manipulam o select escondido 5171 var dxQtyVal = parseInt($qty.val() || "1", 10) || 1; 5172 var dxQtyMax = $qty.find("option").length || 1; 5173 var stepperPart = $qty.length ? [ 5174 "<span class=\"shopcart-drawer__stepper\">", 5175 "<button type=\"button\" class=\"dx-step-minus", (dxQtyVal <= 1 ? " is-remove" : ""), "\" aria-label=\"Reduzir quantidade\">
5175", (dxQtyVal <= 1 ? "<i class=\"fa fa-trash\"></i>" : "−"), "</button>", 5176 "<span class=\"dx-step-qty\">", dxQtyVal, "</span>", 5177 "<button type=\"button\" class=\"dx-step-plus\" aria-label=\"Aumentar quantidade\"", (dxQtyVal >= dxQtyMax ? " disabled" : ""), ">+</button>", 5178 "</span>" 5179 ].join("") : ""; 5180 5181 items.push([ 5182 "<div class=\"shopcart-drawer__item\">", 5183 " <div class=\"shopcart-drawer__thumb\">", $img.length ? $("<div></div>").append($img).html() : "", (dxQtyVal > 1 ? "<span class=\"pvx-qty-badge\">" + dxQtyVal + "</span>" : ""), "</div>", 5184 " <div class=\"shopcart-drawer__item-main\">", 5185 " <div class=\"shopcart-drawer__toprow\">", 5186 " <div class=\"shopcart-drawer__item-title\">", $detail.html() || "", variantText ? "<span class=\"shopcart-drawer__item-variant\">" + variantText + "</span>" : "", "</div>", 5187 pricesPart, 5188 " </div>", 5189 " <div class=\"shopcart-drawer__line-foot\">", stepperPart, totalPart, "</div>", 5190 $qty.length ? " <span class=\"shopcart-drawer__line-controls\" style=\"display:none\">" + $("<div></div>").append($qty).html() + "</span>" : "", 5191 " </div>", 5192 " <div class=\"shopcart-drawer__item-remove\" style=\"display:none\">", $("<div></div>").append($remove).html(), "</div>", 5193 "</div>" 5194 ].join("")); 5195 }); 5196 5197 return items.join(""); 5198 } 5199 5200 // nome da zona/pais efetivo escolhido, para o banner de portes gratis do drawer 5201 function drawerFreeShipZoneName() { 5202 var $opt = $("#delivery_country option:selected"); 5203 if (!$opt.length || !$opt.val()) $opt = $("#country option:selected"); 5204 if ($opt.length && $opt.val()) return $.trim($opt.text()); 5205 if (typeof drawerCountryName === "function") { var n = $.trim(drawerCountryName() || ""); if (n) return n; } 5206 return ""; 5207 } 5208 5209 function renderDrawerFreeShipping(data, labels, currency) { 5210 var $box = $("[data-cart-drawer-free-shipping]"); 5211 if (!$box.length) return; 5212 var threshold = parseFloat(data.free_shipping_threshold_cambio || data.free_shipping_threshold || 0); 5213 if (!threshold || threshold <= 0) { 5214 $box.hide().empty(); 5215 return; 5216 } 5217 5218 var progress = Math.max(0, Math.min(100, parseFloat(data.free_shipping_progress || 0))); 5219 var thresholdText = data.free_shipping_threshold_cambio || data.free_shipping_threshold || "0.00"; 5220 var remainingText = data.free_shipping_remaining_cambio || data.free_shipping_remaining || "0.00"; 5221 var qualified = String(data.free_shipping_qualified || "0") === "1" || parseFloat(data.free_shipping_remaining || 0) <= 0; 5222 if (qualified) { 5223 // ja tem portes gratis: o banner deixa de ter utilidade (a linha Portes diz "Gratis") 5224 $box.hide().empty(); 5225 return; 5226 } 5227 var statusText = labels.freeShippingRemaining2.replace("%s", currency + " " + remainingText); 5228 // {N} = pais efectivo (servidor / select / prefs) â nunca hardcode PT 5229 var fsZone = (data && data.ship_country_name) ? String(data.ship_country_name) : ""; 5230 if (!fsZone) fsZone = drawerFreeShipZoneName(); 5231 if (!fsZone && window.CV2_PREFS_COUNTRY_NAME) fsZone = String(window.CV2_PREFS_COUNTRY_NAME); 5232 var fromLabel = String(labels.freeShippingFrom).replace("{N}", fsZone || ""); 5233 fromLabel = fromLabel.replace(/\s{2,}/g, " ").replace(/\s+from\s*$/i, " from").replace(/\s+a partir de\s*$/i, " a partir de"); 5234 5235 $box.html([ 5236 "<div class=\"shopcart-drawer__free-shipping-inner\" style=\"padding:13px 14px;border:1px solid #eadfe5;border-radius:8px;background:#fffafa;\">", 5237 " <div class=\"shopcart-drawer__free-shipping-line\" style=\"display:grid;grid-template-columns:22px minmax(0,1fr);gap:10px;align-items:center;color:#332c30;\">", 5238 " <i class=\"fa fa-truck\" aria-hidden=\"true\" style=\"font-size:18px;line-height:1;color:#332c30;\"></i>", 5239 " <span style=\"min-width:0;font-size:14px;line-height:1.25;font-weight:700;\">
5239", fromLabel, " ", currency, " ", thresholdText, "</span>", 5240 " </div>", 5241 " <div class=\"shopcart-drawer__free-shipping-bar\" aria-hidden=\"true\" style=\"display:block;height:7px;margin:11px 0 9px;overflow:hidden;border-radius:999px;background:#e7deda;\"><span style=\"display:block;height:100%;border-radius:999px;background:#8d1530;width:", progress, "%;transition:width 260ms ease;\"></span></div>", 5242 " <strong class=\"shopcart-drawer__free-shipping-status\" style=\"display:block;color:#8d1530;font-size:14px;line-height:1.25;font-weight:800;text-align:right;\">", statusText, "</strong>", 5243 "</div>" 5244 ].join("")).show(); 5245 } 5246 5247 function parseDrawerMoney(text) { 5248 var value = String(text || "").replace(/[^0-9,.-]/g, ""); 5249 if (value.indexOf(",") > -1 && value.indexOf(".") > -1) { 5250 value = value.replace(/\./g, "").replace(",", "."); 5251 } else { 5252 value = value.replace(",", "."); 5253 } 5254 var parsed = parseFloat(value); 5255 return isNaN(parsed) ? 0 : parsed; 5256 } 5257 5258 function formatDrawerMoney(value) { 5259 return (Math.max(0, value || 0)).toFixed(2); 5260 } 5261 5262 function drawerVisibleItemsTotal() { 5263 var total = 0; 5264 currentPanel().find(".shopcart-drawer__price-total").each(function () { 5265 total += parseDrawerMoney($(this).text()); 5266 }); 5267 return total; 5268 } 5269 5270 // footer in-flow + CTA pinned (2026-09-17): lista nao precisa de pad de overlay 5271 function syncDrawerFooterPad() { 5272 $(".shopcart-drawer__items").each(function () { 5273 this.style.removeProperty("padding-bottom"); 5274 }); 5275 } 5276 5277 function drawerTotalsPlaceholder(labels) { 5278 return [ 5279 "<div class=\"shopcart-drawer__totals\" aria-live=\"polite\">", 5280 " <div><span>", labels.subtotal, "</span><strong data-cart-drawer-total=\"subtotal\">-</strong></div>", 5281 " <div style=\"display:none\"><span>", labels.shipping, "</span><strong data-cart-drawer-total=\"shipping\">-</strong></div>", 5282 " <div style=\"display:none\"><span data-cart-drawer-savings-label>", labels.savings, "</span><strong data-cart-drawer-total=\"savings\">-</strong></div>", 5283 " <div class=\"shopcart-drawer__promo\">", 5284 " <button type=\"button\" class=\"shopcart-drawer__promo-toggle\" data-dx-promo-toggle>", labels.promoQuestion, "</button>", 5285 " <div class=\"shopcart-drawer__promo-form\" data-dx-promo-form style=\"display:none\">", 5286 " <span class=\"dx-promo-fieldwrap\">", 5287 " <input type=\"text\" data-dx-promo-input placeholder=\"", labels.promoPlaceholder, "\" value=\"", drawerDiscountCode, "\">", 5288 " <button type=\"button\" class=\"dx-promo-hide\" data-dx-promo-hide aria-label=\"Fechar\">×</button>", 5289 " </span>", 5290 " <button type=\"button\" data-dx-promo-apply>", (drawerDiscountCode ? labels.promoRemove : labels.promoApply), "</button>", 5291 " </div>", 5292 " </div>", 5293 " <div class=\"shopcart-drawer__total\"><span data-cart-drawer-total-label>", labels.total, "</span><strong data-cart-drawer-total=\"total\">-</strong></div>", 5294 "</div>" 5295 ].join(""); 5296 } 5297 5298 // codigo de desconto aplicado no drawer (persiste entre rebuilds do footer; 5299 // mesmo contrato do checkout: validate_discount_code + discount_code no populate) 5300 var drawerDiscountCode = (window.cv2GetDiscount ? window.cv2GetDiscount() : ""); 5301 5302 // pais de entrega: prefs (globo) > localStorage "Calcular" > vazio 5303 // t-cart-prefs-ship: cv2_country_id antigo (ex. 173) nao pode ganhar a escolha NL nas prefs 5304 var prefsCountryBoot = (window.CV2_PREFS_COUNTRY ? String(window.CV2_PREFS_COUNTRY) : ""); 5305 var drawerCountry = (window.cv2GetCountry ? window.cv2GetCountry() : ""); 5306 if (prefsCountryBoot) { 5307 if (!drawerCountry || String(drawerCountry) !== String(prefsCountryBoot)) { 5308 drawerCountry = prefsCountryBoot; 5309 if (window.cv2SetCountry) window.cv2SetCountry(prefsCountryBoot); 5310 } 5311 } 5312 var drawerCountryList = null; 5313 5314 function renderDrawerCountrySelect() { 5315 var $slot = $("[data-cart-drawer-total=shipping]"); 5316 if (!$slot.length) return; 5317 function build() { 5318 var opts = ["<option value=\"\">...</option>"]; 5319 for (var i = 0; i < drawerCountryList.length; i++) { 5320 var c = drawerCountryList[i]; 5321 opts.push("<option value=\"" + c.id + "\"" + (String(c.id
5321) === String(drawerCountry) ? " selected" : "") + ">" + drawerIsoFlag(c.iso) + c.nome + "</option>"); 5322 } 5323 $slot.removeAttr("data-dx-ship-calc").css("cursor", "").html("<select class=\"shopcart-drawer__ship-select\" data-dx-ship-select>" + opts.join("") + "</select>"); 5324 } 5325 if (drawerCountryList) { build(); return; } 5326 $.post(REQUEST_ENDPOINT, { ajax: "dx_country_list" }).done(function (resp) { 5327 try { drawerCountryList = (typeof resp === "object") ? resp : JSON.parse(resp); } catch (e) { drawerCountryList = null; } 5328 if (drawerCountryList && drawerCountryList.length) build(); 5329 }); 5330 } 5331 5332 $(document) 5333 .off("click.dxShipCalc") 5334 .on("click.dxShipCalc", "[data-dx-ship-calc]", function () { 5335 renderDrawerCountrySelect(); 5336 }); 5337 5338 $(document) 5339 .off("change.dxShipSelect") 5340 .on("change.dxShipSelect", "[data-dx-ship-select]", function () { 5341 var v = $(this).val(); 5342 if (!v) return; 5343 drawerCountry = v; 5344 if (window.cv2SetCountry) window.cv2SetCountry(v); 5345 refreshDrawerTotals(); 5346 }); 5347 5348 // ISO 3166-1 alpha-2 -> emoji de bandeira (regional indicators; ES5 via surrogate pairs) 5349 function drawerIsoFlag(iso) { 5350 if (!iso || !/^[A-Za-z]{2}
5350$/.test(iso)) return ""; 5351 function rp(ch) { 5352 var n = 0x1F1E6 + ch.toUpperCase().charCodeAt(0) - 65 - 0x10000; 5353 return String.fromCharCode(0xD800 + (n >> 10), 0xDC00 + (n & 0x3FF)); 5354 } 5355 return rp(iso.charAt(0)) + rp(iso.charAt(1)) + " "; 5356 } 5357 5358 function drawerCountryName() { 5359 if (!drawerCountryList) return ""; 5360 for (var i = 0; i < drawerCountryList.length; i++) { 5361 if (String(drawerCountryList[i].id) === String(drawerCountry)) return drawerIsoFlag(drawerCountryList[i].iso) + drawerCountryList[i].nome; 5362 } 5363 return ""; 5364 } 5365 5366 $(document) 5367 .off("click.dxPromoClear") 5368 .on("click.dxPromoClear", "[data-dx-promo-clear]", function (e) { 5369 e.stopPropagation(); 5370 drawerDiscountCode = ""; 5371 if (window.cv2SetDiscount) window.cv2SetDiscount(""); 5372 $("[data-dx-promo-input]").val(""); 5373 $("[data-dx-promo-apply]").text(drawerLabels().promoApply); 5374 try { $.post("/cv2-discount.php", { action: "remove" }); } catch (eC) {} 5375 refreshDrawerTotals(); 5376 refreshDrawerFromServer(); 5377 }); 5378 5379 $(document) 5380 .off("click.dxShipClear") 5381 .on("click.dxShipClear", "[data-dx-ship-clear]", function (e) { 5382 e.stopPropagation(); 5383 drawerCountry = ""; 5384 if (window.cv2SetCountry) window.cv2SetCountry(""); 5385 refreshDrawerTotals(); 5386 }); 5387 5388 $(document) 5389 .off("click.dxStepPlus") 5390 .on("click.dxStepPlus", ".shopcart-drawer__item .dx-step-plus", function (e) { 5391 if (e) { e.preventDefault(); e.stopPropagation(); } 5392 var $sel = $(this).closest(".shopcart-drawer__item").find(".shopcart-drawer__qty-select").first(); 5393 var v = parseInt($sel.val(), 10) || 1; 5394 if (v < ($sel.find("option").length || 1)) $sel.val(String(v + 1)).trigger("change"); 5395 }); 5396 5397 $(document) 5398 .off("click.dxStepMinus") 5399 .on("click.dxStepMinus", ".shopcart-drawer__item .dx-step-minus", function (e) { 5400 if (e) { e.preventDefault(); e.stopPropagation(); } 5401 var $item = $(this).closest(".shopcart-drawer__item"); 5402 var $sel = $item.find(".shopcart-drawer__qty-select").first(); 5403 var v = parseInt($sel.val(), 10) || 1; 5404 if (v <= 1) $item.find(".shopcart-drawer__remove").first().trigger("click"); 5405 else $sel.val(String(v - 1)).trigger("change"); 5406 }); 5407 5408 $(document) 5409 .off("click.dxPromoToggle") 5410 .on("click.dxPromoToggle", "[data-dx-promo-toggle]", function () { 5411 // escopo ao bloco clicado: drawer e checkout partilham esta mecanica 5412 $(this).hide(); 5413 var $f = $(this).closest(".shopcart-drawer__promo").find("[data-dx-promo-form]"); 5414 if (!$f.length) $f = $("[data-dx-promo-form]"); 5415 $f.css("display", "flex"); 5416 $f.find("[data-dx-promo-input]").trigger("focus"); 5417 }); 5418 5419 $(document) 5420 .off("click.dxPromoHide") 5421 .on("click.dxPromoHide", "[data-dx-promo-hide]", function () { 5422 var $p = $(this).closest(".shopcart-drawer__promo"); 5423 if ($p.length) { $p.find("[data-dx-promo-form]").hide(); $p.find("[data-dx-promo-toggle]").show(); } 5424 else { $("[data-dx-promo-form]").hide(); $("[data-dx-promo-toggle]").show(); } 5425 }); 5426 5427 $(document) 5428 .off("click.dxPromoApply") 5429 .on("click.dxPromoApply", "[data-dx-promo-apply]", function () { 5430 var $btn = $(this); 5431 var labels = drawerLabels(); 5432 if (drawerDiscountCode) { 5433 drawerDiscountCode = ""; 5434 if (window.cv2SetDiscount) window.cv2SetDiscount(""); 5435 $("[data-dx-promo-input]").val(""); 5436 $btn.text(labels.promoApply); 5437 try { $.post("/cv2-discount.php", { action: "remove" }); } catch (eR) {} 5438 refreshDrawerTotals(); 5439 refreshDrawerFromServer(); 5440 return; 5441 } 5442 var $scopeInput = $btn.closest(".shopcart-drawer__promo").find("[data-dx-promo-input]"); 5443 if (!$scopeInput.length) $scopeInput = $("[data-dx-promo-input]"); 5444 var code = $.trim($scopeInput.val() || ""); 5445 if (!code) { 5446 if (typeof toastr !== "undefined") toastr.warning("Por favor introduza um código de desconto."); 5447 return; 5448 } 5449 var emailGuess = ($("#email").val() || $("#sc_email").val() || "").toString(); 5450 function cv2deLegacyApply(code) { 5451 $.ajax({ type: "POST", url: REQUEST_ENDPOINT, cache: false, data: { ajax: "validate_discount_code", discount_code: code } }).done(function (resp) { 5452 var clean = (resp || "").toString().trim(); 5453 if (!isNaN(clean) && parseFloat(clean) > 0) { 5454 drawerDiscountCode = code; 5455 if (window.cv2SetDiscount) window.cv2SetDiscount(code); 5456 $btn.text(labels.promoRemove); 5457 if (typeof toastr !== "undefined") toastr.success("Desconto aplicad
5457o com sucesso."); 5458 refreshDrawerTotals(); 5459 refreshDrawerFromServer(); 5460 } else { 5461 $("[data-dx-promo-input]").val(""); 5462 if (typeof toastr !== "undefined") toastr.error("Código de desconto inválido."); 5463 } 5464 }).fail(function () { 5465 if (typeof toastr !== "undefined") toastr.error("Não foi possÃvel validar o código de desconto."); 5466 }); 5467 } 5468 $.ajax({ type: "POST", url: "/cv2-discount.php", cache: false, data: (function(){ var d={ action: "apply", code: code }; if (emailGuess) d.email = emailGuess; return d; })() }).done(function (resp) { 5469 var r = resp; 5470 if (typeof r === "string") { try { r = JSON.parse(r); } catch (e0) { r = null; } } 5471 if (r && r.reason === "fallback") { cv2deLegacyApply(code); return; } 5472 if (r && r.ok) { 5473 drawerDiscountCode = code; 5474 if (window.cv2SetDiscount) window.cv2SetDiscount(code); 5475 $btn.text(labels.promoRemove); 5476 if (typeof toastr !== "undefined") toastr.success(r.message || "Desconto aplicado com sucesso."); 5477 refreshDrawerTotals(); 5478 refreshDrawerFromServer(); 5479 } else { 5480 $("[data-dx-promo-input]").val(""); 5481 var msg = (r && r.message) ? r.message : "Código de desconto inválido."; 5482 if (typeof toastr !== "undefined") toastr.error(msg); 5483 } 5484 }).fail(function () { cv2deLegacyApply(code); }); 5485 }); 5486 5487 function refreshDrawerTotals(retryCount) { 5488 retryCount = retryCount || 0; 5489 var uid = parseInt($("#uid").val(), 10) || 0; 5490 var country = $("#country").val(); 5491 var userCountry = $("#user_country").val(); 5492 var prefsCountry = (window.CV2_PREFS_COUNTRY ? String(window.CV2_PREFS_COUNTRY) : "") || prefsCountryBoot || ""; 5493 // t1644: ficha manda 5494 if (!country && userCountry) country = userCountry; 5495 if (country) $("#user_country").val(country); 5496 // t-cart-prefs-ship: fora do checkout, prefs/drawerCountry estimam portes (guest E login) 5497 var onCheckout = $(".checkout-modern.checkout-v2 #country").length > 0; 5498 if (!onCheckout) { 5499 if (drawerCountry) country = drawerCountry; 5500 else if (prefsCountry) country = prefsCountry; 5501 } 5502 // t1644: no checkout o formulario manda (login ou guest) 5503 if (onCheckout && $("#country").val()) { 5504 country = $("#country").val(); 5505 $("#user_country").val(country); 5506 } 5507 var drawerDeliveryMethod = (typeof checkSelectedDeliveryMethod === "function") ? checkSelectedDeliveryMethod() : 0; 5508 var drawerApiPoints = (drawerDeliveryMethod === "sc_delivery_ctt" || drawerDeliveryMethod === "sc_delivery_worten") ? ($("#api_points").val() || 0) : 0; 5509 5510 $.ajax({ 5511 type: "POST", 5512 url: REQUEST_ENDPOINT, 5513 data: { 5514 ajax: "populate_shopcart", 5515 uid: uid, 5516 country: country, 5517 api_points: drawerApiPoints, 5518 delivery_country: (!onCheckout && (drawerCountry || prefsCountry)) ? (drawerCountry || prefsCountry) : $("#delivery_country").val(), 5519 delivery_method: drawerDeliveryMethod, 5520 payment_method: (typeof checkSelectedPaymentMethod === "function") ? checkSelectedPaymentMethod() : 0, 5521 special_service: $("#special_service").val(), 5522 discount_code: drawerDiscountCode || (window.cv2GetDiscount ? window.cv2GetDiscount() : "") || "" 5523 } 5524 }).done(function (resp) { 5525 var data = {}; 5526 try { data = (typeof resp === "object") ? resp : JSON.parse(resp); } catch (e) { data = {}; } 5527 var labels = drawerLabels(); 5528 var currency = drawerCurrency(); 5529 var shippingValue = parseFloat(data.shipping || 0); 5530 var discountPercent = parseFloat(data.discount_percent || 0); 5531 var savingsLabel = labels.savings + (discountPercent > 0 ? " (" + discountPercent + "%)" : ""); 5532 var subtotalText = data.subtotal_nodiscount_cambio || data.subtotal_cambio || "0.00"; 5533 var totalText = data.bigtotal_cambio || "0.00"; 5534 var visibleItemsTotal = drawerVisibleItemsTotal(); 5535 var serverSubtotal = parseDrawerMoney(subtotalText); 5536 var serverTotal = parseDrawerMoney(totalText); 5537 if (visibleItemsTotal > 0 && serverSubtotal <= 0 && serverTotal <= 0) { 5538 subtotalText = formatDrawerMoney(visibleItemsTotal); 5539 totalText = formatDrawerMoney(visibleItemsTotal); 5540 shippingValue = 0; 5541 data.free_shipping_qualified = 0; 5542 data.free_shipping_threshold = 0; 5543 data.free_shipping_threshold_cambio = 0; 5544 if (retryCount < 2) { 5545 setTimeout(function () { refreshDrawerTotals(retryCount + 1); }, 350); 5546 } 5547 } 5548 5549 // Portes definitivos apenas com morada conhecida (flag do servidor); 5550 // guest ve "A calcular" + "Total estimado" sem portes somados. 5551 var shippingKnown = String(data.shipping_known || "0") === "1"; 5552 var effectiveKnown = shippingKnown || !!drawerCountry || !!prefsCountry; 5553 if (data.ship_country_id && !onCheckout && !drawerCountry && prefsCountry) { 5554 drawerCountry = String(data.ship_country_id); 5555 } 5556 $(".shopcart-drawer__footer").toggleClass("is-shipping-pending", !effectiveKnown); 5557 5558 $("[data-cart-drawer-total=subtotal]").text(currency + " " + subtotalText); 5559 $("[data-cart-drawer-total=shipping]").closest("div").show().find("[data-dx-ship-tag]").remove(); 5560 if (!effectiveKnown) { 5561 // guest sem pais: CTA "Calcular" abre o select de paises de entrega 5562 $("[data-cart-drawer-total=shipping]").removeAttr("data-dx-ship-calc").css("cursor", "").html("<button type=\"button\" class=\"shopcart-drawer__ship-calc\" data-dx-ship-calc>" + labels.shippingCalc + "</button>"); 5563 var estimatedTotal = Math.max(0, parseDrawerMoney(totalText) - parseFloat(data.shipping_cambio || 0)); 5564 $("[data-cart-drawer-total-label]").text(labels.totalEstimated); 5565 $("[data-cart-drawer-total=total]").text(currency + " " + formatDrawerMoney(estimatedTotal) + " " + labels.plusShipping); 5566 } else { 5567 if (String(data.free_shipping_qualified || "0") === "1" || parseFloat(data.shipping_cambio || 0) === 0) { 5568 // portes a 0 mostram sempre "Gratis" (nunca "⬠0.00") 5569 $("[data-cart-drawer-total=shipping]").text(labels.shippingFree); 5570 } else { 5571 $("[data-cart-drawer-total=shipping]").text(currency + " " + (data.shipping_cambio || "0.00")); 5572 } 5573 if (!shippingKnown && drawerCountry) { 5574 // guest com pais escolhido: tag com X junto ao label; fechar volta ao "Calcular" 5575 var dxCountryName = drawerCountryName(); 5576 if (dxCountryName) { 5577 $("[data-cart-drawer-total=shipping]").closest("div").find("span").first().append( 5578 " <span class=\"shopcart-drawer__ship-tag\" data-dx-ship-tag>" + dxCountryName + 5579 "<button type=\"button\" data-dx-ship-clear aria-label=\"" + (labels.promoRemove || gl("CHECKOUT_SUM
5579MARY_REMOVE_CODE") || "Remover") + "\">×</button></span>" 5580 ); 5581 } 5582 } 5583 $("[data-cart-drawer-total-label]").text(labels.total); 5584 $("[data-cart-drawer-total=total]").text(currency + " " + totalText); 5585 } 5586 var discountVal = parseFloat(data.discount_cambio || data.discount || 0); 5587 var $savRow = $("[data-cart-drawer-total=savings]").closest("div"); 5588 $savRow.find("[data-dx-promo-tag]").remove(); 5589 if (discountVal > 0) { 5590 $savRow.show(); 5591 $("[data-cart-drawer-total=savings]").text("-" + currency + " " + (data.discount_cambio || "0.00")); 5592 $("[data-cart-drawer-savings-label]").text(savingsLabel); 5593 if (drawerDiscountCode) { 5594 // codigo aplicado: tag com X junto ao label; fechar remove o desconto 5595 $("[data-cart-drawer-savings-label]").append( 5596 " <span class=\"shopcart-drawer__ship-tag\" data-dx-promo-tag>" + drawerDiscountCode + 5597 "<button type=\"button\" data-dx-promo-clear aria-label=\"" + (labels.promoRemove || gl("CHECKOUT_SUMMARY_REMOVE_CODE") || "Remover") + "\">×</button></span>" 5598 ); 5599 } 5600 } else { 5601 $savRow.hide(); 5602 } 5603 // com codigo aplicado a linha "Tem um codigo de desconto?" desaparece 5604 $(".shopcart-drawer__promo").toggle(!drawerDiscountCode); 5605 renderDrawerFreeShipping(data, labels, currency); 5606 syncDrawerFooterPad(); 5607 window.setTimeout(syncDrawerFooterPad, 350); 5608 }); 5609 } 5610 5611 function refreshSavedForLater() { 5612 var $slot = $currentSavedSlot(); 5613 if (!$slot.length) return; 5614 if (!shopcartSavedForLaterAvailable()) { 5615 $slot.empty().hide(); 5616 return; 5617 } 5618 $.post(REQUEST_ENDPOINT, { ajax: "shopcart_saved_for_later_html" }, function (response) { 5619 var data = {}; 5620 try { data = (typeof response === "object") ? response : JSON.parse(response); } catch (e) { data = {}; } 5621 if (data.ok && data.html) { 5622 $slot.html(data.html).show(); 5623 applySavedForLaterState($slot); 5624 } else { 5625 $slot.empty().hide(); 5626 } 5627 }); 5628 } 5629 5630 function refreshDrawerFromServer() { 5631 setShopcartDrawerLoading(true); 5632 // o servidor le o discount_code de $_POST (nao da sessao); sem ele, um 5633 // utilizador logado renderiza o desconto VIP em vez do codigo aplicado, 5634 // deixando os precos/badges por-produto desatualizados 5635 var code = drawerDiscountCode || (window.cv2GetDiscount ? window.cv2GetDiscount() : "") || ""; 5636 $.post(REQUEST_ENDPOINT, { ajax: "generate_shopcart", type: "html", discount_code: code }).done(function (html) { 5637 injectContent(currentPanel(), html); 5638 }).always(function () { 5639 setShopcartDrawerLoading(false); 5640 refreshCheckoutAfterCartChange(); 5641 }); 5642 } 5643 5644 function $currentSavedSlot() { 5645 return currentPanel().find("[data-cart-saved-for-later]"); 5646 } 5647 5648 function injectContent($panel, html) { 5649 if (!$panel || !$panel.length) return; 5650 var labels = drawerLabels(); 5651 var $content = $("<div></div>").html(html); 5652 var itemsHtml = buildDrawerItems($content); 5653 var lang = Cookies.get("clang_id") || $("#session_lang").val() || "pt"; 5654 var footerHtml = [ 5655 "<div class=\"shopcart-drawer__free-shipping\" data-cart-drawer-free-shipping style=\"display:none\"></div>", 5656 drawerTotalsPlaceholder(labels), 5657 "<div class=\"cv2-dx\" id=\"cv2_dx\">", 5658 " <div class=\"cv2-dx__title\">", labels.expressTitle, "</div>", 5659 " <div class=\"cv2-dx__note\">", labels.expressNote, "</div>", 5660 " <div class=\"cv2-dx__row\">", 5661 " <div class=\"cv2-dx__slot\" id=\"cv2_dx_paypal_slot\" style=\"display:none\"></div>", 5662 " <button type=\"button\" class=\"cv2-dx__btn cv2-dx__btn--paypal\" id=\"cv2_dx_paypal\" aria-label=\"PayPal\"><span class=\"cv2-pp-word\"><b>Pay</b>Pal</span></button>", 5663 " <button type=\"button\" class=\"cv2-dx__btn cv2-dx__btn--gpay is-soon\" id=\"cv2_dx_gpay\" aria-label=\"Google Pay\" disabled><svg xmlns='http://www.w3.org/2000/svg' fill='none' preserveAspectRatio='xMidYMid' viewBox='0 0 126 50' focusable='false' class='pvx-gpay-svg'><g fill-rule='evenodd' clip-rule='evenodd'><path fill='#fff' d='M59.62 7.342v12.636h7.793q2.785 0 4.605-1.872c1.247-1.244 1.872-2.73 1.872-4.447q0-2.525-1.872-4.401-1.82-1.918-4.605-1.919H59.62zm0 17.083v14.657h-4.654V2.895h12.348q4.7 0 7.99 3.132 3.343 3.133 3.343 7.632c0 3.066-1.115 5.629-3.343 7.68-2.157 2.059-4.824 3.083-7.993 3.083h-7.69zm23.73 7.078q0 1.818 1.545 3.033c1.027.804 2.234 1.21 3.615 1.21 1.959 0 3.7-.724 5.236-2.17 1.538-1.451 2.302-3.153 2.302-5.106q-2.176-1.718-6.072-1.718c-1.887 0-3.466.458-4.728 1.368-1.266.91-1.897 2.034-1.897 3.383Zm6.023-17.996q5.16.002 8.148 2.754 2.982 2.758 2.984 7.558v15.263h-4.454v-3.435h-.2q-2.887 4.244-7.694 4.246-4.096-.002-6.855-2.427-2.757-2.427-2.758-6.065 0-3.843 2.91-6.116 2.909-2.274 7.767-2.274c2.76 0 5.042.504 6.827 1.516v-1.064q0-2.426-1.922-4.116a6.6 6.6 0 0 0-4.502-1.693c-2.6 0-4.655 1.092-6.174 3.287l-4.098-2.581q3.392-4.852 10.02-4.853m36.683.81L110.519 50h-4.806l5.769-12.484-10.219-23.198h5.06l7.387 17.791h.099l7.186-17.794z'/><path fill='#4285F4' d='M40.81 21.267c0-1.464-.123-2.878-.359-4.228H20.818v8.01h11.247a9.62 9.62 0 0 1-4.16 6.319v5.199h6.713c3.93-3.62 6.192-8.975 6.192-15.3'/><path fill='#34A853' d='M20.819 41.585c5.62 0 10.348-1.841 13.799-5.016l-6.713-5.203c-1.866 1.257-4.27 1.99-7.087 1.99-5.428 0-10.039-3.658-11.685-8.584H2.219v5.357a20.82 20.82 0 0 0 18.6 11.46'/><path fill='#FABB05' d='M9.133 24.771a12.5 12.5 0 0 1 0-7.96v-5.356H2.219A20.7 20.7 0 0 0 0 20.788
5663c0 3.358.805 6.53 2.219 9.337l6.914-5.357z'/><path fill='#E94235' d='M20.818 8.229c3.07 0 5.818 1.052 7.985 3.12v.002l5.942-5.935C31.142 2.06 26.438 0 20.82 0A20.82 20.82 0 0 0 2.222 11.457l6.914 5.357c1.646-4.927 6.257-8.585 11.685-8.585'/></g></svg></button>", 5664 " <button type=\"button\" class=\"cv2-dx__btn cv2-dx__btn--apple is-soon\" id=\"cv2_dx_apple\" aria-label=\"Apple Pay\" disabled> Pay</button>", 5665 " </div>", 5666 " <div class=\"cv2-dx__or\"><span>", labels.expressOr, "</span></div>", 5667 "</div>", 5668 "<button type=\"button\" class=\"shopcart-drawer__checkout\">", labels.checkout, "</button>" 5669 ].join(""); 5670 5671 var isEmpty = !itemsHtml || $content.find(".shopcart_empty, .shopcart-drawer__empty").length > 0; 5672 var wrappedHtml = [ 5673 "<div class=\"shopcart-drawer__head\">", 5674 " <div>", 5675 " <strong>", labels.title, "</strong>", 5676 " </div>", 5677 " <button type=\"button\" class=\"shopcart-drawer__close\" aria-label=\"", labels.close, "\"><i class=\"fa fa-times\"></i></button>", 5678 "</div>", 5679 "<div class=\"shopcart-drawer__body\">", 5680 " <div class=\"shopcart-drawer__items\">", itemsHtml, "<div class=\"shopcart-drawer__saved-slot\" data-cart-saved-for-later></div></div>", 5681 (!isEmpty && itemsHtml) ? "<div class=\"shopcart-drawer__footer cv2-has-items\">" + footerHtml + "</div>" : "", 5682 "</div>" 5683 ].join(""); 5684 var $modal = $panel.find("#shopcart-modal"); 5685 if ($modal.length) { 5686 $modal.html(wrappedHtml); 5687 } else { 5688 $panel.html(wrappedHtml); 5689 } 5690 $panel.toggleClass("is-empty", isEmpty); 5691 if ($modal.length) $modal.toggleClass("is-empty", isEmpty); 5692 $panel.find(".shopcart-drawer__checkout").toggle(!isEmpty).prop("disabled", isEmpty); 5693 if (isEmpty) { 5694 $panel.find(".shopcart-drawer__checkout").attr("aria-disabled", "true").hide(); 5695 $panel.find(".shopcart-drawer__footer").removeClass("cv2-has-items").hide(); 5696 } 5697 if (itemsHtml && !isEmpty) refreshDrawerTotals(); 5698 refreshSavedForLater(); 5699 } 5700 5701 function openMini() { 5702 var $panel = currentPanel(); 5703 if (!$panel.length) return; 5704 if (!$panel.parent().is("body")) $panel.appendTo("body"); 5705 $("body").addClass("shopcart-drawer-open"); 5706 // keep drawer rendered so CSS transform can animate 5707 setShopcartDrawerLoading(true, $panel); 5708 $.ajax({ 5709 type: "POST", 5710 url: REQUEST_ENDPOINT, 5711 data: { ajax: "generate_shopcart", type: "html" } 5712 }).done(function (html) { 5713 injectContent($panel, html); 5714 }).always(function () { 5715 setShopcartDrawerLoading(false, $panel); 5716 }); 5717 } 5718 5719 function closeMini() { 5720 setShopcartDrawerLoading(true); 5721 setTimeout(function () { 5722 $("body").removeClass("shopcart-drawer-open"); 5723 setShopcartDrawerLoading(false); 5724 }, 160); 5725 } 5726 5727 window.openShopcartDrawer = openMini; 5728 window.closeShopcartDrawer = closeMini; 5729 window.renderShopcartDrawerHtml = function (html) { 5730 injectContent(currentPanel(), html); 5731 }; 5732 5733 $(document) 5734 .off("click.shopcartDrawer keyup.shopcartDrawer") 5735 .on("click.shopcartDrawer", ".shopcart-drawer__close, #shopcart-drawer-overlay", function (e) { 5736 e.preventDefault(); 5737 // X roda e desvanece antes do drawer fechar 5738 var $x = $(this).hasClass("shopcart-drawer__close") ? $(this) : $(".shopcart-drawer__close"); 5739 $x.addClass("is-closing"); 5740 window.setTimeout(function () { 5741 closeMini(); 5742 window.setTimeout(function () { $x.removeClass("is-closing"); }, 400); 5743 }, 240); 5744 }) 5745 .on("click.shopcartDrawerCheckout", ".shopcart-drawer__checkout", function (e) { 5746 e.preventDefault(); 5747 if ($(this).prop("disabled") || $(this).closest(".is-empty").length) return; 5748 if (currentPanel().find(".shopcart_empty, .shopcart-drawer__empty").length && !currentPanel().find(".shopcart-drawer__item").length) return; 5749 var lang = Cookies.get("clang_id") || $("#session_lang").val() || "pt"; 5750 window.location.assign("/" + lang + "/checkout/"); 5751 }) 5752 .on("click.shopcartEmptyCta", "[data-empty-cta]", function (e) { 5753 e.preventDefault(); 5754 var href = this.getAttribute("href") || ("/" + (Cookie
5754s.get("clang_id") || $("#session_lang").val() || "pt") + "/novidades/"); 5755 closeMini(); 5756 window.setTimeout(function () { window.location.assign(href); }, 220); 5757 }) 5758 .on("click.shopcartSavedToggle", "[data-saved-toggle]", function (e) { 5759 e.preventDefault(); 5760 var $section = $(this).closest(".shopcart-drawer__saved-section"); 5761 $section.toggleClass("is-collapsed"); 5762 setSavedForLaterCollapsed($section.hasClass("is-collapsed")); 5763 }) 5764 .on("click.shopcartSavedAdd", "[data-saved-add-to-cart]", function (e) { 5765 e.preventDefault(); 5766 var $row = $(this).closest(".shopcart-drawer__saved-item"); 5767 var $section = $(this).closest(".shopcart-drawer__saved-section"); 5768 setShopcartDrawerLoading(true); 5769 $.post(REQUEST_ENDPOINT, { 5770 ajax: "shopcart_saved_for_later_add_to_cart", 5771 id_lista: $section.data("saved-list-id") || 0, 5772 id_code: $row.data("product-id") || 0, 5773 id_produto_elemento: $row.data("element-id") || 0, 5774 qtd: parseInt($row.find("[data-saved-qty]").first().val() || "1", 10) || 1 5775 }).always(refreshDrawerFromServer); 5776 }) 5777 .on("click.shopcartSavedAddAll", "[data-saved-add-all]", function (e) { 5778 e.preventDefault(); 5779 var $section = $(this).closest(".shopcart-drawer__saved-section"); 5780 var listId = $section.data("saved-list-id") || 0; 5781 setShopcartDrawerLoading(true); 5782 $.post(REQUEST_ENDPOINT, { ajax: "shopcart_saved_for_later_add_all_to_cart", id_lista: listId }).always(refreshDrawerFromServer); 5783 }) 5784 .on("click.shopcartSavedRemove", "[data-saved-remove]", function (e) { 5785 e.preventDefault(); 5786 var $row = $(this).closest(".shopcart-drawer__saved-item"); 5787 var $section = $(this).closest(".shopcart-drawer__saved-section"); 5788 setShopcartDrawerLoading(true); 5789 $.post(REQUEST_ENDPOINT, { 5790 ajax: "shopcart_saved_for_later_remove", 5791 id_lista: $section.data("saved-list-id") || 0, 5792 id_code: $row.data("product-id") || 0, 5793 id_produto_elemento: $row.data("element-id") || 0 5794 }).always(refreshDrawerFromServer); 5795 }) 5796 .on("keyup.shopcartDrawer", function (e) { 5797 if (e.keyCode === 27) closeMini(); 5798 }); 5799 } 5800 5801 // expõe global, caso outros scripts legados usem 5802 window.check_shopcart = check_shopcart; 5803 // exportado: o promo partilhado com o drawer (checkout-modern) refresca o 5804 // resumo do checkout apos aplicar/remover codigo; o afterLogin do OTP ja 5805 // testava window.populate_shopcart sem o export existir 5806 window.populate_shopcart = populate_shopcart; 5807 window.generate_shopcart = generate_shopcart; 5808 window.refreshCheckoutAfterCartChange = refreshCheckoutAfterCartChange; 5809 5810 /* ========================================================== 5811 * 11) TRACKING UTM 5812 * ========================================================== */ 5813 (function utmOnce() { 5814 if (sessionStorage.getItem('utm_source')) { 5815 console.log('Source already tracked:', sessionStorage.getItem('utm_source'), sessionStorage.getItem('utm_medium'), sessionStorage.getItem('utm_campaign')); 5816 return; 5817 } 5818 function getQueryParams() { return Object.fromEntries(new URLSearchParams(window.location.search)); } 5819 var p = getQueryParams(); 5820 var gclid = p.gclid, explicitSrc = p.utm_source, explicitMed = p.utm_medium, explicitCam = p.utm_campaign; 5821 var ref = document.referrer, landing = window.location.pathname; 5822 var source = explicitSrc, medium = explicitMed, campaign = explicitCam; 5823 5824 if (!source && gclid) { source = 'google'; medium = 'cpc'; } 5825 if (!source && ref) { 5826 var host = new URL(ref).hostname.replace(/^www\./, ''); 5827 var socials = ['facebook.com','instagram.com','twitter.com','linkedin.com']; 5828 if (socials.some(d => host.endsWith(d))) { source = host.split('.')[0]; medium = 'social'; } 5829 else if (/(google\.[a-z]+|bing\.com|yahoo\.)/.test(host)) { source = host.includes('google.') ? 'google' : host.split('.')[0]; medium = 'organic'; } 5830 else if (/(mail\.google\.com|outlook\.live\.com|mail\.yahoo\.com)/.test(host)) { source = 'newsletter'; medium = 'email'; } 5831 else { source = host; medium = 'referral'; } 5832 } 5833 if (!source) { source = 'direct'; medium = 'none'; } 5834 5835 sessionStorage.setItem('utm_source', source); 5836 sessionStorage.setItem('utm_medium', medium); 5837 sessionStorage.setItem('utm_campaign', campaign || ''); 5838 sessionStorage.setItem('referrer', ref); 5839 sessionStorage.setItem('landingPage', landing); 5840 console.log('Tracked source:', source, medium, campaign); 5841 })(); 5842
5843 // ==== FAVORITES MENU (global, compat com master antigo) ==== 5844 window.favoritesMenu = function () { 5845 $.ajax({ 5846 type: "POST", 5847 url: REQUEST_ENDPOINT, 5848 cache: false, 5849 async: true, 5850 data: { ajax: "favoritesMenu" }, 5851 success: function (html) { 5852 // todos os alvos possÃveis 5853 $(".favorites-menu, #favorites-menu, .divfavorites-menu").html(html); 5854 5855 // se tiver lazy carregado 5856 if ($.fn.lazy) { 5857 $(".lazy").Lazy(); 5858 } 5859 }, 5860 error: function (xhr) { 5861 console.warn("favoritesMenu() failed:", xhr.status); 5862 } 5863 }); 5864 }; 5865 5866 if ($(".favorites-menu, #favorites-menu, .divfavorites-menu").length) { 5867 window.favoritesMenu(); 5868 } 5869 5870 // ==== bloco de favoritos no carrinho (/cart/) ==== 5871 window.favoritesSHreminder = function () { 5872 $.ajax({ 5873 type: "POST", 5874 url: REQUEST_ENDPOINT, 5875 cache: false, 5876 data: { ajax: "favoritesSHreminder" }, 5877 success: function (html) { 5878 var $slot = $("#favoritesSHreminder, .favoritesSHreminder"); 5879 var clean = (html || "").trim(); 5880 5881 if (!clean) { 5882 // nada para mostrar â esconde 5883 $slot.empty().hide(); 5884 return; 5885 } 5886 5887 $slot.html(clean).show(); 5888 5889 if ($.fn.lazy) { 5890 $(".lazy").lazy({ 5891 bind: "event", 5892 effect: "fadeIn", 5893 effectTime: 500, 5894 threshold: 0 5895 }); 5896 } 5897 } 5898 }); 5899 }; 5900 5901 // Reload/navigate: topo. Back/forward: não forçar 0 (cv2-scroll-restore.js). 5902 function resetScrollOnReload() { 5903 if (!hasWindow) return; 5904 var isBack = false; 5905 try { 5906 var nav = performance.getEntriesByType && performance.getEntriesByType("navigation")[0]; 5907 if (nav && nav.type === "back_forward") isBack = true; 5908 else if (performance.navigation && performance.navigation.type === 2) isBack = true; 5909 } catch (eNav) {} 5910 if (window.history && "scrollRestoration" in window.history) { 5911 try { window.history.scrollRestoration = "manual"; } catch (e) {} 5912 } 5913 if (isBack) return; 5914 window.scrollTo(0, 0); 5915 $(window).scrollTop(0); 5916 } 5917 5918/* ===== BOOT ===== */ 5919$(function () { 5920 resetScrollOnReload(); 5921 ensureLangMeta(); 5922 // CV2: bootstrap do token CSRF (mesma origem; usado por login/OTP/password) 5923 window.cv2Csrf = window.cv2Csrf || ""; 5924 $.post(REQUEST_ENDPOINT, { ajax: "cv2_csrf" }, function (r) { 5925 try { var j = typeof r === "object" ? r : JSON.parse(r); if (j && j.csrf) window.cv2Csrf = j.csrf; } catch (e) {} 5926 }); 5927 fetchLangDict().always(function(){ 5928 setFiltersActiveState(false); 5929 initState(); 5930 var currentCategory = getFilterCategoryKey(); 5931 var savedCategory = getSavedFilterCategory(); 5932 var snapshot = loadFiltersSnapshot(currentCategory); 5933 var urlFiltersState = currentCategory ? readFiltersStateFromUrl() : null; 5934 var categoryChanged = currentCategory && savedCategory && savedCategory !== currentCategory; 5935 fpLog("boot", { 5936 currentCategory: currentCategory, 5937 savedCategory: savedCategory, 5938 categoryChanged: categoryChanged, 5939 hasSnapshot: snapshot ? true : false, 5940 hasUrlFilters: urlFiltersState ? true : false 5941 }); 5942 shouldResetFiltersOnBoot = !!categoryChanged; 5943 if (currentCategory && !savedCategory) { 5944 rememberFilterCategory(currentCategory); 5945 } 5946 bindEvents(); 5947 bindMiniCartHover(); 5948 check_shopcart(); 5949 5950 var hasSnapshot = snapshot && hasSessionFilters(snapshot); 5951 var shouldRestoreSnapshot = false; 5952 5953 if (urlFiltersState) { 5954 postJSON("restoreFilters", { value: urlFiltersState }, function () { 5955 lastSessionFilters = urlFiltersState; 5956 rememberFilterCategory(currentCategory); 5957 syncThenRender({ resetAll: false }); 5958 }); 5959 } else if (shouldRestoreSnapshot) { 5960 var tasks = buildFilterTasksFromSnapshot(currentCategory, snapshot); 5961 runTasksSequentially(tasks, function () { 5962 rememberFilterCategory(currentCategory); 5963 syncThenRender({ resetAll: false }); 5964 }); 5965 } else { 5966 syncThenRender({ resetAll: shouldResetFiltersOnBoot }); 5967 } 5968 5969 if ($("#shopcart_html").length) { 5970 generate_shopcart("html"); 5971 } 5972 5973 if ($("#favoritesSHreminder").length || $(".favoritesSHreminder").length) { 5974 window.favoritesSHreminder(); 5975 } 5976 5977 initAccountSection(); 5978 5979 }); 5980}); 5981 5982})(window.jQuery); 5983 5984 function toggleLeftMenu() { 5985 var $fullWrapper = $("#full-wrapper"); 5986 var $mobileMenu = $("#webstore-mobile-menu"); 5987 5988 if (!$fullWrapper.hasClass("full-wrapper-open")) { 5989 $mobileMenu.addClass("webstore-mobile-menu-open").show(); 5990 } 5991 5992 $("#q").val(""); 5993 $("#livesearch-reset, #livesearch").hide();
5994 $("#main-container, #topm-cats, #topm-info").removeClass("main-container-search"); 5995 $("#livesearch").html("").scrollTop(0); 5996 5997 $fullWrapper.toggleClass("full-wrapper-open"); 5998 5999 if ($fullWrapper.hasClass("full-wrapper-open")) { 6000 $("head").append("<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>"); 6001 $("#hamburguer").html("<i class='fa fa-times fa-2x mobile-menu-icon' aria-hidden='true'></i>"); 6002 $("body, html").addClass("disable_hscroll"); 6003 $("#main-container").addClass("main-container-search"); 6004 6005 setTimeout(function () { 6006 $(".livesearch-container, .header-left-mobile-banner, .header-right-mobile-banner, #main-container, .branding").addClass("lowopacity"); 6007 }, 400); 6008 } else { 6009 $(".mobile-menu-container").scrollTop(0); 6010 $("head").append("<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=yes'>"); 6011 $("#hamburguer").html("<i class='fa fa-bars fa-2x mobile-menu-icon' aria-hidden='true'></i>"); 6012 $("body, html").removeClass("disable_hscroll"); 6013 $("#main-container").removeClass("main-container-search"); 6014 $mobileMenu.hide(); 6015 $(".livesearch-container, .header-left-mobile-banner, .header-right-mobile-banner, #main-container, .branding").removeClass("lowopacity"); 6016 } 6017 } 6018 6019 6020$(function () { 6021 6022 // RESET PWD 6023 if ($("#reset_password").length > 0) { 6024 $("#reset_password").on("submit", function (e) { 6025 e.preventDefault(); 6026 if ($("#resethash").val() && $("#resetpass").val()) { 6027 $.post("/request.php", { 6028 ajax: "redefinePWD", 6029 formhash: Base64.encode($("#resethash").val()), 6030 formpwd: Base64.encode($("#resetpass").val()) 6031 }, 6032 function (data, textStatus, jqXHR) { 6033 if (data == "OK") { 6034 var swalTitle = (typeof gl === "function" && gl("swal_info")) || "Informação"; 6035 var swalMsg = (typeof gl === "function" && gl("swal_recoverpass_success_message")) || "Nova Password definida com sucesso!"; 6036 var swalOk = (typeof gl === "function" && gl("swal_btnok")) || "OK"; 6037 6038 if (typeof swal === "function") { 6039 swal({ 6040 title: swalTitle, 6041 text: swalMsg, 6042 type: "success", 6043 confirmButtonColor: "#ABD559",
6044 showCancelButton: false, 6045 animation: false, 6046 confirmButtonText: swalOk, 6047 closeOnConfirm: true 6048 }, function () { window.location.href = "/"; }); 6049 } else { 6050 alert(swalMsg); 6051 window.location.href = "/"; 6052 } 6053 } else { 6054 alert("Não foi possÃvel redefinir a sua nova password, por favor tente mais tarde"); 6055 } 6056 }, 6057 "json" 6058 ); 6059 } else { 6060 console.log("error"); 6061 } 6062 }); 6063 6064 } 6065});
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.