1// @TODO : refactor cart JS : to many outdated functions / redundant code 2 3document.addEventListener('DOMContentLoaded', onContentLoad); 4window.productDetailHandler = productDetailHandler; 5window.dataLayer = window.dataLayer || []; 6window.currentTransportModeElm; 7 8/** 9 * Helper global pour les conditionnements de vente. 10 * Contrat partagé avec les templates Twig : ne pas renommer les méthodes ni changer leurs signatures. 11 * 12 * Données attendues sur l'objet article/ligne : 13 * - qte_in_stock : nombre 14 * - conditionnement.vente : { id_colisage_type: qte_vente } 15 * - colisage_types : [ { id_colisage_type, actif_vente (bool), qte_vente } ] 16 * 17 * Règle : si l'article a >= 1 conditionnement de vente actif, la quantité dispo 18 * = floor(stock / plus_petit_conditionnement) ; sinon stock normal. 19 */ 20window.lmbConditionnement = { 21 /** 22 * Renvoie le plus petit qte_vente parmi les colisage_types actifs en vente (qte > 0), sinon null. 23 * @param {Object} item - L'objet article / ligne panier 24 * @returns {number|null} 25 */ 26 plusPetit(item) { 27 if (!item || !item.colisage_types) { 28 return null; 29 } 30 31 const colisages = Array.isArray(item.colisage_types) 32 ? item.colisage_types 33 : Object.values(item.colisage_types); 34 35 let plusPetit = null; 36 37 colisages.forEach(colisage => { 38 if (!colisage || !colisage.actif_vente) { 39 return; 40 } 41 42 // La qté par colisage peut venir du champ direct (qte_vente) ou de conditionnement.vente[id] 43 let qte = parseFloat(colisage.qte_vente); 44 if (isNaN(qte) && item.conditionnement && item.conditionnement.vente) { 45 qte = parseFloat(item.conditionnement.vente[colisage.id_colisage_type]); 46 } 47 48 if (!isNaN(qte) && qte > 0 && (plusPetit === null || qte < plusPetit)) { 49 plusPetit = qte; 50 } 51 }); 52 53 return plusPetit; 54 }, 55 /** 56 * @param {Object} item - L'objet article / ligne panier 57 * @returns {boolean} - true si l'article est commandable au regard de son stock et de son conditionnement 58 */ 59 enStock(item) { 60 const c = lmbConditionnement.plusPetit(item); 61 const q = parseFloat(item.qte_in_stock) || 0; 62 return c ? q >= c : q > 0; 63 }, 64 /** 65 * Nombre de colisages commandables (ou stock brut si pas de conditionnement actif). 66 * @param {Object} item - L'objet article / ligne panier 67 * @returns {number} 68 */ 69 maxColisages(item) { 70 const c = lmbConditionnement.plusPetit(item); 71 const q = parseFloat(item.qte_in_stock) || 0; 72 return c ? Math.floor(q / c) : q; 73 }, 74 /** 75 * Quantité totale à venir en réapprovisionnement (brut), somme des entrées restocking. 76 * @param {Object} item - L'objet article / ligne panier (avec restocking ou article_info.restocking) 77 * @returns {number} 78 */ 79 totalReappro(item) { 80 const restocking = (item && (item.restocking || (item.article_info && item.article_info.restocking))) || []; 81 if (!Array.isArray(restocking)) { 82 return 0; 83 } 84 return restocking.reduce((sum, r) => sum + (parseFloat(r && r.quantity) || 0), 0); 85 }, 86 /** 87 * Plafond de colisages commandables en tenant compte du réapprovisionnement à venir. 88 * Utilisé pour le mode vente hors stock « autorisé si réappro » (vente_hors_stock == 2) : 89 * on peut commander jusqu'à stock + réappro, pas seulement le stock. 90 * @param {Object} item - L'objet article / ligne panier 91 * @returns {number} 92 */ 93 maxColisagesAvecReappro(item) { 94 const c = lmbConditionnement.plusPetit(item); 95 const q = (parseFloat(item.qte_in_stock) || 0) + lmbConditionnement.totalReappro(item); 96 return c ? Math.floor(q / c) : q; 97 }, 98 /** 99 * Plafond de commande effectif d'une ligne selon son mode de vente hors stock. 100 * - mode 2 (autorisé si réappro) : stock + réappro 101 * - autres modes bornés (0 / forbidden) : stock seul 102 * - mode illimité (1) : null (pas de plafond) 103 * @param {Object} item - L'objet article / ligne panier 104 * @param {number|string} venteHorsStock - config vente hors stock de la ligne 105 * @param {string} outOfStockMode - out_of_stock_mode de la ligne 106 * @returns {number|null} plafond en colisages, ou null si pas de plafond 107 */ 108 maxCommandable(item, venteHorsStock, outOfStockMode) { 109 const vhs = parseInt(venteHorsStock); 110 if (vhs === 2) { 111 return lmbConditionnement.maxColisagesAvecReappro(item); 112 } 113 if (vhs === 1 && outOfStockMode !== 'forbidden') { 114 return null; // vente hors stock illimitée 115 } 116 return lmbConditionnement.maxColisages(item); 117 }, 118 /** 119 * Libellé (lib_colisage_type) du plus petit conditionnement de vente actif, sinon null.
120 * Pendant JS de helperService::plusPetitConditionnementVenteInfos()['lib']. 121 * @param {Object} item - L'objet article / ligne panier 122 * @returns {string|null} 123 */ 124 libPlusPetit(item) { 125 if (!item || !item.colisage_types) { 126 return null; 127 } 128 129 const colisages = Array.isArray(item.colisage_types) 130 ? item.colisage_types 131 : Object.values(item.colisage_types); 132 133 let lib = null; 134 let plusPetit = null; 135 136 colisages.forEach(colisage => { 137 if (!colisage || !colisage.actif_vente) { 138 return; 139 } 140 141 let qte = parseFloat(colisage.qte_vente); 142 if (isNaN(qte) && item.conditionnement && item.conditionnement.vente) { 143 qte = parseFloat(item.conditionnement.vente[colisage.id_colisage_type]); 144 } 145 146 if (!isNaN(qte) && qte > 0 && (plusPetit === null || qte < plusPetit)) { 147 plusPetit = qte; 148 lib = colisage.lib_colisage_type || null; 149 } 150 }); 151 152 return lib; 153 }, 154 /** 155 * Compose le détail de quantité affiché à côté du statut de stock. 156 * Avec conditionnement : « 168 Carton de 40 disponibles » (suffixe paramétrable, ex. « à venir »). 157 * Sans conditionnement : « 6720 unités disponibles ». 158 * @param {Object} item - L'objet article / ligne panier 159 * @param {number} qte - Quantité déjà exprimée en colisages (maxColisages) ou stock brut. 160 * @param {Object} labels - { dispo: 'disponibles', unite: 'unités' } (chaînes traduites injectées par le template). 161 * @returns {string} 162 */ 163 detailQuantite(item, qte, labels) { 164 const lib = lmbConditionnement.libPlusPetit(item); 165 const cond = lmbConditionnement.plusPetit(item); 166 if (lib && cond) { 167 // « 168 Carton de 40 disponibles » 168 return `${qte} ${lib} de ${cond} ${labels.dispo}`; 169 } 170 // « 6720 unités disponibles » 171 return `${qte} ${labels.unite} ${labels.dispo}`; 172 } 173}; 174 175if(typeof configuratorPage === 'undefined') { 176 var configuratorPage = window.drupalSettings?.gezy?.configurator?.display_template || 'dedicated_page'; 177} 178 179if(typeof configuratorFormat === 'undefined') { 180 var configuratorFormat = window.drupalSettings?.gezy?.configurator?.format || 'standard'; 181} 182if(typeof cartLineManagement === 'undefined') { 183 var cartLineManagement = window.drupalSettings?.gezy?.configurator?.cart_line_management || 'standard'; 184} 185function onContentLoad() { 186 const currentURL = window.location.href; 187 if (currentURL.indexOf("order-confirmation") !== -1) { 188 pushDataLayerPurchase(); 189 // Sur la page de confirmation, le panier a été transformé en commande : il ne 190 // faut PAS appeler /cart/content (sinon getCurrent -> getPanier(uuid_cũ) où 191 // le panier est devenu commande -> LMB crée un NOUVEAU panier et Drupal 192 // écrase le cookie uuid_panier -> un panier vide « fantôme » apparaît). 193 // On se contente de réinitialiser le badge quantité du header à 0. 194 resetCartQtyBadge(); 195 return; 196 } 197 loadCartQty(); 198 199 if (currentURL.indexOf("panier") !== -1) { 200 pushCartItem(); 201 } 202} 203 204/** Met le badge de quantité du header à 0 (panier vidé) sans aucun appel API. 205 * Utilisé sur /order-confirmation pour éviter la création d'un panier fantôme. */ 206function resetCartQtyBadge() { 207 const cartMenuItem = document.querySelector('header a.cart_link'); 208 if (!cartMenuItem) return; 209 cartMenuItem.setAttribute('data-cart-qty', 0); 210 const totalsItem = document.querySelector('.totalitems'); 211 if (totalsItem) { 212 totalsItem.textContent = '(0)'; 213 } 214 const qtyBadge = cartMenuItem.querySelector('.qty-badge'); 215 if (qtyBadge) { 216 qtyBadge.innerHTML = 0; 217 } 218} 219 220function pushDataLayerPurchase() { 221 let data_article = JSON.parse(htmlDecode(localStorage.getItem('article_data'))); 222 let option_data_storage = JSON.parse(htmlDecode(localStorage.getItem('option_data'))); 223 let option_data = { 224 'transaction_id': option_data_storage.transaction_id, 225 'shipping': option_data_storage.shipping, 226 'tax': option_data_storage.tax, 227 'currency': option_data_storage.currency, 228 } 229 pushMutilDataLayer(data_article, "purchase", "view_cart", "View cart", option_data) 230 231 localStorage.removeItem('article_data'); 232 localStorage.removeItem('option_data'); 233} 234 235function increaseCount(a, b) { 236 var input = b.previousElementSibling; 237 var value = parseInt(input.value, 10); 238 value = isNaN(value) ? 0 : value; 239 value++; 240 input.value = value; 241} 242 243function decreaseCount(a, b) { 244 var input = b.nextElementSibling; 245 var value = parseInt(input.value, 10); 246 if (value > 1) { 247 value = isNaN(value) ? 0 : value; 248 value--; 249 input.value = value; 250 } 251} 252 253let newAddress = document.getElementById("shipping-new-address"); 254let kAddress = document.getElementById("shipping-keep"); 255let nAddress = document.getElementById("name-address"); 256let aAddress = document.getElementById("address-address"); 257let zAddress = document.getElementById("zip-address"); 258let cAddress = document.getElementById("city-address"); 259var useNewAddress = 0; 260 261function setNewAddress() { 262 newAddress.style.display = "block"; 263 kAddress.style.display = "block"; 264 // nAddress.setAttribute("required", ""); 265 // aAddress.setAttribute("required", ""); 266 // zAddress.setAttribute("required", ""); 267 // cAddress.setAttribute("required", ""); 268 useNewAddress = 1; 269} 270 271function keepAddress() { 272 newAddress.style.display = "none"; 273 kAddress.style.display = "none"; 274 // nAddress.removeAttribute("required"); 275 // aAddress.removeAttribute("required"); 276 // zAddress.removeAttribute("required"); 277 // cAddress.removeAttribute("required"); 278 useNewAddress = 0; 279} 280 281function strictProductSearch() { 282 const keyword = $("#quick-order .keyword").val(); 283 if (!keyword) return null; 284 285 const messages = new Drupal.Message(); 286 const url = getUrl('strictProductSearch'); 287 const body = getFormData({ keyword: keyword }); 288 fetch(url, { 289 method: 'POST', 290 headers: { 291 'Accept': 'application/json', 292 }, 293 body 294 }) 295 .then(response => response.json()) 296 .then(json => { 297 if (!json.status) { 298 messages.add( 299 Drupal.t('Erreur lors de la recherche des produits'), 300 { type: 'error' } 301 ); 302 return null; 303 } else if (json.products && !json.products.length) { 304 messages.add( 305 Drupal.t('Article non trouvé'), 306 { type: 'error' } 307 ); 308 return null; 309 } 310 311 if (json.products.length === 1) { 312 addQuicklyToCart(json.products[0]); 313 } else { 314 Alpine.store('modal').open({ 315 id: 'result_list_product', 316 products: json.products, 317 }); 318 } 319 }); 320} 321 322function addQuicklyToCart(article = {}) { 323 const messages = new Drupal.Message(); 324 325 if (article.statut_declinaison === 2) {
326 getProductChild(article.id_article).then((res) => { 327 const productChilds = res.filter(obj => obj.statut_declinaison === 1); 328 Alpine.store('modal').open({ 329 id: 'list_product_child', 330 qty: 1, 331 productChilds, 332 }); 333 }); 334 return; 335 } else { 336 // On n'ouvre la popup de choix que s'il y a plusieurs conditionnements : l'utilisateur a alors un 337 // choix à faire. Avec un seul conditionnement (ou aucun), il n'y a rien à choisir, on ajoute 338 // directement avec ce conditionnement pré-sélectionné (transmis plus bas via details_qte). 339 if ((Object.keys(article?.conditionnement?.vente || {}).length > 1)) { 340 Alpine.store('modal').open({ 341 id: 'conditionnement', 342 article: article, 343 selectedCondition: setSelectedConditionnement(article) 344 }); 345 return; 346 } 347 } 348 function setSelectedConditionnement(article) { 349 let selectedCondition = null; 350 if(article.conditionnement?.vente) { 351 const conditionnementKeys = Object.keys(article.conditionnement?.vente).map(String); 352 const vente_id = article?.colisage?.types_fav.vente; 353 // Loop through `colisage_types` and add matching `<option>` elements 354 if (article?.colisage_types && article?.colisage) { 355 const colisageArray = Object.values(article.colisage_types); 356 const validColisages = colisageArray.filter(colisage => 357 conditionnementKeys.includes(colisage.id_colisage_type.toString()) 358 ); 359 let hasFav = false; 360 colisageArray 361 .filter(colisage => conditionnementKeys.includes(colisage.id_colisage_type.toString())) // Filter valid items 362 .forEach(colisage => { 363 const val = `${colisage.id_colisage_type};${colisage.lib_colisage_type};${article.conditionnement.vente[colisage.id_colisage_type] || 0}`; 364 if (vente_id == colisage.id_colisage_type) { 365 selectedCondition = val; 366 hasFav = true; 367 } 368 }); 369 370 // If no match was found, set to the first valid item 371 if (!hasFav && validColisages.length > 0) { 372 const firstColisage = validColisages[0]; 373 selectedCondition = `${firstColisage.id_colisage_type};${firstColisage.lib_colisage_type};${article.conditionnement.vente[firstColisage.id_colisage_type] || 0}`; 374 } 375 } 376 } 377 378 return selectedCondition; 379 } 380 381 382 if (article.vente_hors_stock == null || article.vente_hors_stock === 0 || article.is_orderable_art === 0) { 383 if (article.qte_in_stock <= 0) { 384 messages.add( 385 Drupal.t('Cet article n\'est actuellement pas disponible en stock'), 386 { type: 'error' } 387 ); 388 return; 389 } 390 } 391 392 // Conditionnement unique (ou aucun) pré-sélectionné : on le transmet pour que l'ajout direct (sans 393 // popup) applique bien le conditionnement, comme le ferait la popup pour un produit à plusieurs 394 // conditionnements. 395 const details_qte = setSelectedConditionnement(article); 396 const data = { product_id: article.id_article, qty: 1 }; 397 if (details_qte) { 398 data.details_qte = details_qte; 399 } 400 cartAjax("cart/content", "POST", data, ""); 401} 402 403function loadCartQty(cart = null, opts = {}) { 404 const { forceFetch = false, showModal = false } = opts; 405 const cartMenuItem = document.querySelector('header a.cart_link'); 406 if (!cartMenuItem) { 407 return null; 408 } 409 410 const setQtyFromCartContent = (cart) => { 411 const content = cart?.contenu; 412 413 if (!Array.isArray(content)) { 414 return null; 415 } 416 417 let qty = content.reduce((count, contenu) => { 418 let detail = null; 419 try { 420 detail = JSON.parse(contenu.details_qte); 421 } catch (err) { 422 console.warn(err); 423 } 424 425 if (detail?.origine == 'MODE_LIVRAISON') { 426 return 0; 427 } 428 429 return count + parseInt(contenu.qte || 0); 430 }, 0); 431 432 if (qty) { 433 cartMenuItem.setAttribute('data-cart-qty', qty); 434 if (qtyBadge = cartMenuItem.querySelector('.qty-badge')) {
435 qtyBadge.innerHTML = qty; 436 } 437 } 438 }; 439 440 const setTotalQty = () => { 441 const url = getUrl('panier/current/item_count'); 442 fetch(url, { 443 method: 'GET', 444 headers: { 445 'Accept': 'application/json', 446 } 447 }).then(response => response.json()).then(json => { 448 if (json?.total_qte) { 449 cartMenuItem.setAttribute('data-cart-qty', json.total_qte); 450 const totalsItem = document.querySelector('.totalitems'); 451 if (totalsItem) { 452 totalsItem.textContent = '(' + json.total_qte + ')'; 453 } 454 if (qtyBadge = cartMenuItem.querySelector('.qty-badge')) { 455 qtyBadge.innerHTML = json.total_qte; 456 } 457 } 458 }) 459 } 460 461 try { 462 if (cart && !forceFetch) { 463 setTotalQty(); 464 } else { 465 const url = getUrl('cart/content'); 466 fetch(url, { 467 method: 'GET', 468 headers: { 469 'Accept': 'application/json', 470 } 471 }) 472 .then(response => response.json()) 473 .then(json => { 474 setTotalQty(); 475 if (showModal) { 476 Alpine.store('modal').open({ 477 id: 'added_to_cart', 478 cart: json?.cart, 479 }); 480 } 481 }); 482 } 483 } catch (err) { 484 console.error('Error loading cart Qty : ', err); 485 } 486} 487 488function pushCartItem() { 489 const url = getUrl('cart/content'); 490 491 fetch(url, { 492 method: 'GET', 493 headers: { 494 'Accept': 'application/json', 495 } 496 }) 497 .then(response => response.json()) 498 .then(json => { 499 const content = json?.cart?.contenu || null; 500 if (!content?.length) { 501 return null 502 } 503 504 let data_cart_info = json?.cart; 505 let option_data = { 506 'currency': data_cart_info.abrev_devise, 507 } 508 509 pushMutilDataLayer(content, "view_cart", "view_cart", "View cart", option_data) 510 }); 511} 512 513/** 514 * Met à jour la quantité à ajouter dans le panier 515 * 516 * @param {boolean} decrease - Si true, on décrémente la quantité (sinon on l'augmente) 517 */ 518function updateQty(decrease = false) { 519 let qty = parseInt($("#qty").val()); 520 qty = calcQty(qty, decrease); 521 $("#qty").val(qty); 522} 523 524/** 525 * Indique si une option de personnalisation doit être saisie par le client 526 * 527 * @param {{}} option - L'option de personnalisation 528 */ 529function isPersonnalisationRequired(option) { 530 return option?.saisie_obligatoire == 1; 531} 532 533/** 534 * Renvoie les options de personnalisation obligatoires qui n'ont pas été renseignées 535 * 536 * @param {{}} article_infos 537 * @param {number|string} product_id - L'id de l'article (pour les options de type fichier) 538 */ 539function getMissingPersonnalisations(article_infos, product_id) { 540 const options = article_infos?.personnalisationsParentOptions ?? []; 541 542 return options.filter(option => { 543 if (!isPersonnalisationRequired(option)) { 544 return false; 545 } 546 // Le fichier n'est pas porté par l'option : il est stocké au moment de l'upload 547 if (option.ref_carac_type === "asset") { 548 return !localStorage.getItem('file_option_' + product_id + '_' + option.id_carac); 549 } 550 551 return !option.valeur || String(option.valeur).trim() === ""; 552 }); 553} 554 555/** 556 * Signale les options de personnalisation obligatoires non renseignées et 557 * indique s'il faut bloquer l'ajout au panier 558 * 559 * @param {{}} article_infos 560 * @param {number|string} product_id 561 */ 562function checkPersonnalisations(article_infos, product_id) { 563 const options = article_infos?.personnalisationsParentOptions ?? []; 564 options.forEach(option => { option.error = false; }); 565 566 const missing = getMissingPersonnalisations(article_infos, product_id); 567 if (!missing.length) { 568 return true; 569 } 570 571 const messages = new Drupal.Message(); 572 missing.forEach(option => { 573 option.error = true; 574 messages.add( 575 Drupal.t('Le champ "@title" est obligatoire', { 576 '@title': option.label || option.lib_carac, 577 }), 578 { type: 'error' } 579 ); 580 }); 581 582 return false; 583} 584 585/** 586 * Ajoute le produit au panier (au clic sur le bouton "Commander") 587 * 588 * @param {DOMElement} form - Le formulaire qui trigger le submit 589 * @param {{}} override_data - Les donnéees pour l'ajout au panier (si on n'a pas de form) 590 * @param article_infos 591 * @param context 592 */ 593function addToCart(form, override_data = {}, article_infos = {}, context = null) { 594 const messages = new Drupal.Message(); 595 596 let data = {}; 597 if (override_data && Object.keys(override_data).length) { 598 data = override_data; 599 } else { 600 data = $(form).serializeArray(); 601 data = data.reduce((obj, item) => { 602 obj[item.name] = item.value; 603 return obj; 604 }, {}); 605 } 606 if (data.qty < 0) { 607 messages.add( 608 Drupal.t('Erreur lors de l\'ajout des produits au panier'), 609 { type: 'error' } 610 ); 611 return false;
612 } 613 let url = getUrl("cart/content"); 614 615 let addedQty = data.qty || 0; 616 let addedId = data.product_id || 0; 617 618 if (!checkPersonnalisations(article_infos, addedId)) { 619 if (context && typeof context === "object") { 620 context.isLoading = false; 621 } 622 return false; 623 } 624 625 let asset = []; 626 if (article_infos?.options) { 627 article_infos?.options.forEach(option => { 628 if(option.ref_carac_type === "asset") { 629 const stored = localStorage.getItem('file_option_' + data.product_id + '_' + option.id_carac); 630 if (stored) { 631 const span = document.getElementById('fileName_' + data.product_id + '_' + option.id_carac); 632 asset.push(JSON.parse(stored)); 633 localStorage.removeItem('file_option_' + data.product_id + '_' + option.id_carac); 634 span.textContent = "Aucun fichier sélectionné"; 635 span.title = "Aucun fichier sélectionné"; 636 } 637 } 638 }); 639 } 640 641 if(asset.length > 0) { 642 data.asset = asset; 643 } 644 645 if ( 646 isProductConfigurable(article_infos) && 647 configuratorPage === 'dedicated_page' 648 ) { 649 let configuratorUrl = new URL(Drupal.url.toAbsolute('/configurateur')); 650 let params = new URLSearchParams(); 651 652 params.append('products[' + addedId + ']', addedQty); 653 654 configuratorUrl.search = params.toString(); 655 656 // Redirect to the configurator route 657 window.location.href = configuratorUrl.toString(); 658 return; 659 } 660 if (context && typeof context === "object") { 661 context.isLoading = true; 662 } 663 664 $.ajax({ 665 method: "POST", 666 url: url, 667 data: { 668 ...data, 669 // "error" ne sert qu'à l'affichage, on ne l'envoie pas 670 personnalisations: (article_infos.personnalisationsParentOptions ?? []).map( 671 ({ error, ...personnalisation }) => personnalisation 672 ), 673 }, 674 }).done(function (response) { 675 const { cart = null } = response; 676 if (!response?.status || !cart) { 677 messages.add( 678 Drupal.t('Erreur lors de l\'ajout des produits au panier'), 679 { type: 'error' } 680 ); 681 if (context) context.isLoading = false; 682 return false; 683 } 684 685 handleCartResponse(article_infos, addedId, addedQty, context); 686 }); 687} 688 689async function addProductsToCart(products = []) { 690 const messages = new Drupal.Message(); 691 const url = getUrl("cart/content"); 692 const body = getFormData({ products }); 693 try { 694 const response = await fetch(url, { 695 method: 'POST', 696 headers: { 697 'Accept': 'application/json', 698 }, 699 body, 700 }); 701 702 const json = await response.json(); 703 704 if (json.status === false || !json?.cart) { 705 messages.add( 706 Drupal.t('Erreur lors de l\'ajout des produits au panier'), 707 { type: 'error' } 708 ); 709 710 return null; 711 } 712 713 return json.cart; 714 } catch (err) { 715 messages.add( 716 Drupal.t('Erreur lors de l\'ajout des produits au panier'), 717 { type: 'error' } 718 ); 719 console.error(err); 720 return false; 721 } 722} 723 724function productDetailHandler(article = {}) { 725 const messages = new Drupal.Message(); 726 return { 727 isLoading: false, 728 activeTab: 0, 729 article: { 730 ...article, 731 points: [], 732 selectedPoint: null, 733 allPoints: [], 734 allPointsTemp: [], 735 dataPoints: [], 736 dataSpareParts: [], 737 currentImage: null, 738 firstLoading: true, 739 }, 740 declinaisons: {}, 741 isChangingDeclinaison: false, 742 effetsEcomArticleCrossSell: [], 743 effetsEcomArticleListe: [], 744 loadingLiaisons: false, 745 showDescription: false, // use for spare-part template 746 showCaracs: false, // use for spare-part template 747 imageDimensions: [], // Track image width: use for spare-part template 748 pointX : null, // Track image width: use for spare-part template 749 pointY : null, // Track image width: use for spare-part template 750 cart: { 751 qty: 1, 752 linkedProducts: [] 753 }, 754 get displayAddToCart() { 755 /* 756 * Workaround for configurator in product page : 757 * If configurator is in product page add to cart button must be disabled, 758 * so button is disabled until we have liaisons 759 * @TODO : add configurable check directly in API response will remove the need for this 760 */ 761 if( 762 configuratorPage == 'product_page' && 763 (this.loadingLiaisons || isProductConfigurable(this.article)) 764 ) { 765 return false;
766 } 767 768 return true; 769 }, 770 get displayConfigurator() { 771 return configuratorPage == 'product_page' && isProductConfigurable(this.article); 772 }, 773 get displayQtyForSimpleConfig() { 774 return this.displayConfigurator && configuratorFormat == 'simple' && cartLineManagement == 'children'; 775 }, 776 increaseQty() { 777 this.cart.qty = calcQty(this.cart.qty, false); 778 // Plafond de commande selon le mode de vente hors stock : stock seul (mode 0/forbidden), 779 // stock + réappro (mode 2), ou aucun plafond (mode 1 illimité). maxCommandable renvoie 780 // null quand il n'y a pas de plafond. 781 if (this.article.type != 'service' && lmbConditionnement.plusPetit(this.article)) { 782 const maxCommandable = lmbConditionnement.maxCommandable(this.article, this.article.vente_hors_stock, this.article.out_of_stock_mode); 783 if (maxCommandable !== null && this.cart.qty > maxCommandable) { 784 this.cart.qty = maxCommandable; 785 } 786 } 787 if(this.displayQtyForSimpleConfig) { 788 document.dispatchEvent(new CustomEvent('qty-cart-changed', { 789 detail: { qty: this.cart.qty }, 790 })); 791 } 792 }, 793 decreaseQty() { 794 this.cart.qty = calcQty(this.cart.qty, true); 795 if(this.displayQtyForSimpleConfig) { 796 document.dispatchEvent(new CustomEvent('qty-cart-changed', { 797 detail: { qty: this.cart.qty }, 798 })); 799 } 800 }, 801 increaseQtyLiaison(selectedPoint) { 802 if (selectedPoint && this.article.allPoints?.length > 0) { 803 this.article.allPoints.forEach(point => { 804 if (point.id_article === selectedPoint.id_article) { 805 point.qty = calcQtyLiaison(selectedPoint.qty, false); 806 selectedPoint.qty = point.qty; 807 // this.article.selectedPoint = selectedPoint; // https://docs.google.com/document/d/1TfQjSXzeP6IgNVCTKJMNd8rpihgJdoxvQIi_fxK4zV8/edit?tab=t.0#bookmark=id.z7i200octpzi 808 } 809 }); 810 } 811 }, 812 decreaseQtyLiaison(selectedPoint) { 813 if (selectedPoint && this.article.allPoints?.length > 0) { 814 this.article.allPoints.forEach(point => { 815 if (point.id_article === selectedPoint.id_article) { 816 point.qty = calcQtyLiaison(selectedPoint.qty, true); 817 selectedPoint.qty = point.qty; 818 //this.article.selectedPoint = selectedPoint; 819 } 820 return point; 821 }); 822 } 823 }, 824 inputQty(event, selectedPoint) { 825 if (selectedPoint && this.article.allPoints?.length > 0) { 826 this.article.allPoints.forEach(point => { 827 if (point.id_article === selectedPoint.id_article) { 828 let temp = +event.target.value.replace(/[^0-9]/g, ''); 829 if (temp > point.qte_in_stock) { 830 temp = point.qte_in_stock 831 } 832 point.qty = temp; 833 selectedPoint.qty = point.qty; 834 // this.article.selectedPoint = selectedPoint; 835 } 836 return point; 837 }); 838 } 839 }, 840 validateNumber(qte_in_stock) { 841 // Skip validation for services or when out of stock sales are allowed (unless specifically forbidden at product level) 842 if (this.article.type == 'service' || (this.article.vente_hors_stock == 1 && this.article.out_of_stock_mode != 'forbidden')) { 843 return; 844 } 845 // Plafond de commande selon le mode de vente hors stock : stock seul (mode 0/forbidden) 846 // ou stock + réappro (mode 2). Si un conditionnement de vente actif existe, la quantité 847 // saisie est un nombre de colisages ; sinon on raisonne en stock brut. 848 const maxCommandableColis = lmbConditionnement.plusPetit(this.article) 849 ? lmbConditionnement.maxCommandable(this.article, this.article.vente_hors_stock, this.article.out_of_stock_mode) 850 : null; 851 if (maxCommandableColis !== null && parseInt(this.cart.qty) > maxCommandableColis) { 852 messages.add( 853 Drupal.t('Quantité insufisante en stock'), 854 { type: 'error' } 855 ); 856 this.cart.qty = maxCommandableColis; 857 return false; 858 } 859 // Sans conditionnement : plafond brut = stock, ou stock + réappro en mode 2. 860 if (!lmbConditionnement.plusPetit(this.article)) { 861 const maxBrut = parseInt(this.article.vente_hors_stock) === 2 862 ? (parseInt(qte_in_stock) || 0) + lmbConditionnement.totalReappro(this.article) 863 : parseInt(qte_in_stock); 864 if (parseInt(this.cart.qty) > maxBrut) { 865 messages.add( 866 Drupal.t('Quantité insufisante en stock'), 867 { type: 'error' } 868 );
869 this.cart.qty = maxBrut; 870 return false; 871 } 872 } 873 }, 874 addToCartHandler(formEl = null, qte_in_stock = null) { 875 // Les options de personnalisation obligatoires doivent être saisies par le client 876 if (!checkPersonnalisations(this.article, this.article.id_article)) { 877 return; 878 } 879 // On bloque l'ajout au panier, en plus du disabled sur le bouton 880 // Check if product is not a service AND (out of stock sales are not allowed OR product-specific out_of_stock_mode is forbidden OR product is not orderable) 881 if (this.article.type != 'service' && (this.article.vente_hors_stock == null || this.article.vente_hors_stock == 0 || this.article.out_of_stock_mode == 'forbidden' || this.article.is_orderable_art == 0)) { 882 if ((qte_in_stock === null && this.article.qte_in_stock <= 0) || (qte_in_stock !== null && qte_in_stock <=0)) { 883 messages.add( 884 Drupal.t('Cet article n\'est actuellement pas disponible en stock'), 885 { type: 'error' } 886 ); 887 return; 888 } 889 // Avec un conditionnement de vente actif, cart.qty est un nombre de colisages : 890 // on plafonne au nombre de colisages commandables. Sinon on garde le plafond stock brut. 891 const maxColisages = lmbConditionnement.plusPetit(this.article) 892 ? lmbConditionnement.maxColisages(this.article) 893 : this.article.qte_in_stock; 894 if (maxColisages < this.cart.qty) { 895 messages.add( 896 Drupal.t('Quantité disponible insuffisante'), 897 { type: 'error' } 898 ); 899 const input = document.querySelector('input#qty[name="qty"]'); 900 input.value = maxColisages; 901 this.cart.qty = maxColisages; 902 return; 903 } 904 } 905 906 let option_data_add_cart = { 907 'qty': this.cart.qty, 908 } 909 pushDataLayer(this.article, "add_to_cart", "catalogue_detail", "Catalogue detail", option_data_add_cart) 910 911 // @TODO - Voir à quoi sert cette vérif, je crois pas qu'on utilise le code après 912 if (!this.cart.linkedProducts?.length) { 913 if (formEl == null) { 914 return addToCart(null, { product_id: this.article.id_article, qty: this.cart.qty }, this.article, this); 915 } else { 916 return addToCart(formEl, {}, this.article, this); 917 } 918 } 919 920 let products = this.cart.linkedProducts 921 922 const data = { 923 id: 'add_liaisons', 924 currentProduct: { 925 id_article: this.article.id_article, 926 qty: this.cart.qty, 927 // "error" ne sert qu'à l'affichage, on ne l'envoie pas 928 personnalisations: (this.article.personnalisationsParentOptions ?? []).map( 929 ({ error, ...personnalisation }) => personnalisation 930 ) 931 }, 932 products, 933 addToCart: () => { 934 const modalData = Alpine.store('modal').instance; 935 const products = [ 936 modalData.currentProduct, 937 ...modalData.products.map(product => { 938 if (product.vente_hors_stock == null || product.vente_hors_stock === 0 || product.is_orderable_art === 0) { 939 if (!product?.qte_in_stock) { 940 messages.add( 941 Drupal.t('Attention, l\'article "%lib" n\'est plus en stock, celui-ci n\'est donc pas ajouté.', { 942 '%lib': product.lib, 943 }), 944 { type: 'warning' } 945 ); 946 return null; 947 } 948 } 949 950 let qty = product.qty || 1; 951 if (product.vente_hors_stock == null || product.vente_hors_stock === 0 || product.is_orderable_art === 0) { 952 if (qty > product.qte_in_stock) { 953 qty = product.qte_in_stock 954 messages.add( 955 Drupal.t('Attention, la quantité sélectionnée pour l\'article "%lib" dépasse la quantié en stock, celle-ci a donc été réduire à "%qty".', { 956 '%lib': product.lib, 957 '%qty': ` ${qty} `, 958 }), 959 { type: 'warning' } 960 ); 961 } 962 } 963 return { id_article: product.id_article, qty } 964 }).filter(product => product) 965 ] 966 967 addProductsToCart(products).then(cart => { 968 if (cart?.contenu?.length) { 969 const totalQty = cart.contenu.reduce((count, item) => { 970 if (products.find(product => product.id_article == item.id_article)) { 971 count += item.qte; 972 } 973 return count 974 }, 0) 975 976 Alpine.store('modal').open({ 977 id: 'added_to_cart', 978 qty: totalQty 979 }); 980 } 981 982 loadCartQty(); 983 }) 984 } 985 }; 986 987 Alpine.store('modal').open(data); 988 989 const url = getUrl('productDetails'); 990 const body = getFormData({ products }); 991 fetch(url, { 992 method: 'POST', 993 headers: { 994 'Accept': 'application/json', 995 }, 996 body, 997 }) 998 .then(response => response.json()) 999 .then(json => { 1000 products = json?.products || json 1001 if (products?.length) { 1002 Alpine.store('modal').instance.products = products; 1003 } 1004 }); 1005 }, 1006 /* 1007 * Fonction pour maj les données de l'article afin de contrer le c
1007ache 1008 */ 1009 async fetchData() { 1010 this.article = this.formatArticleData(this.article) 1011 this.initDeclinaison() 1012 this.getCommonParentPersonnalisations() 1013 // Overlay PRIX/STOCK live : la page peut être servie depuis le cache (HTML figé). 1014 // On rafraîchit prix + stock en live (no_cache) pour qu'ils soient TOUJOURS vrais. 1015 this.refreshPrixStockLive(); 1016 this.loadingLiaisons = true; 1017 1018 const body = getFormData({ 1019 products: [this.article.id_article] 1020 }) 1021 const url = getUrl('productDetails') 1022 1023 const response = await fetch(url, { 1024 method: 'POST', 1025 body 1026 }) 1027 1028 const json = await response.json(); 1029 1030 this.loadingLiaisons = false; 1031 json.products = json.products || []; 1032 1033 if(json.products && Array.isArray(json.products)) { 1034 json.products.forEach((article) => { 1035 let id = article.id_article 1036 if(this.article.id_article != id) { 1037 return 1038 } 1039 this.article.liaisons = article.liaisons; 1040 this.article.dataSpareParts = article.data_spare_parts; 1041 this.initLiaisons(); 1042 // permet de définir une fonction JS côté thème enfant pour pouvoir lancer un JS après le chargement des liaisons 1043 // ex : pour faire un carrousel avec les Articles complémentaires 1044 if(typeof jsAfterInitLiaisons === 'function'){ 1045 jsAfterInitLiaisons(); 1046 } 1047 }) 1048 } 1049 1050 this.$nextTick(() => { 1051 this.article.firstLoading = false; 1052 // Initialize all images dimensions 1053 if (this.article.url_images && this.article.url_images.length > 0) { 1054 this.article.url_images.forEach((img, index) => { 1055 this.onImageLoad(index); 1056 }); 1057 } 1058 this.article.allPoints = this.prepareDataPoints(this.article); 1059 }); 1060 1061 const $slider = $(this.$el).find('#carouselImagePrincipale'); 1062 $slider.on('afterChange', (event, slick, currentSlide) => { 1063 event.preventDefault(); 1064 this.onImageLoad(currentSlide); 1065 }); 1066 1067 // Add window resize listener to update image dimensions 1068 window.addEventListener('resize', () => { 1069 if (this.article.url_images && this.article.url_images.length > 0) { 1070 this.article.url_images.forEach((_, index) => { 1071 this.updateImageDimensions(index); 1072 }); 1073 } 1074 }); 1075 }, 1076 // Rafraîchit le bloc prix (rendu server-side, no_cache) + les champs stock Alpine. 1077 // Best-effort : si l'appel échoue on garde l'affichage server-rendered (pas de régression). 1078 async refreshPrixStockLive() { 1079 try { 1080 const id = this.article.id_article; 1081 if (!id) return; 1082 const response = await fetch(getUrl('prix-stock-live/' + id)); 1083 if (!response.ok) return; 1084 const data = await response.json(); 1085 // 1) PRIX : swap du bloc rendu (macro réutilisée -> promo / à partir de / HT-TTC / éco-part) 1086 if (data.prix_html) { 1087 const oldEco = document.getElementById('ecoPart'); 1088 if (oldEco) oldEco.remove(); 1089 const oldPrix = document.getElementById('prixArticle'); 1090 if (oldPrix) oldPrix.outerHTML = data.prix_html; 1091 } 1092 // 2) STOCK : MAJ des champs -> dispoArticle + bouton ajout panier (Alpine) réagissent en live 1093 if (data.en_stock !== undefined && data.en_stock !== null) this.article.en_stock = data.en_stock; 1094 if (data.qte_in_stock !== undefined) this.article.qte_in_stock = data.qte_in_stock; 1095 if (data.qte_stock_colisages !== undefined) this.article.qte_stock_colisages = data.qte_stock_colisages; 1096 if (data.qte_reappro_colisages !== undefined) this.article.qte_reappro_colisages = data.qte_reappro_colisages; 1097 if (data.is_orderable_art !== undefined) this.article.is_orderable_art = data.is_orderable_art; 1098 if (data.vente_hors_stock !== undefined) this.article.vente_hors_stock = data.vente_hors_stock; 1099 if (data.restocking !== undefined) this.article.restocking = data.restocking || []; 1100 } catch (e) { 1101 console.warn('refreshPrixStockLive', e); 1102 } 1103 }, 1104 // Priorité à la colonne delais_dispo ; la carac Délaidispo reste pour la rétrocompat 1105 // des articles non encore migrés. 1106 resolveDelaiDispo(point) { 1107 const delaiColonne = parseInt(point.delais_dispo, 10) || 0; 1108 if (delaiColonne > 0) return delaiColonne; 1109 1110 const carac = point.list_caracs?.find( 1111 c => c.ref_carac === 'Délaidispo' || c.lib_carac === 'Délaidispo' 1112 ); 1113 return carac ? parseInt(carac.valeur, 10) || 0 : 0; 1114 }, 1115 1116 getStockText(point) { 1117 if (point.qte_in_stock > 0) return 'En stock'; 1118 1119 const delai = this.resolveDelaiDispo(point); 1120 if (delai > 0) return `Disponible sous ${delai} jours`; 1121 1122 return 'Disponible sous quelques jours'; 1123 }, 1124 1125 getStockClass(point) { 1126 const base = 'text-white font-bold px-2 py-1 rounded'; 1127 if (point.qte_in_stock > 0) { 1128 return `${base} bg-green-500`; // En stock 1129 } 1130 if (this.resolveDelaiDispo(point) > 0) { 1131 return `${base} bg-[#EAAA2E]`; // Disponible sous X jours 1132 } 1133 return `${base} bg-red-500`; 1134 }, 1135 // Fonction utilisée pour réaliser toutes les opérations de formattage / calcul des données 1136 // Appelée via l'init du JSON dans le HTML, puis au retour du fetch 1137 formatArticleData(article = {}) { 1138 // On conserve les prix au format numérique (pour les comparaisons) 1139 article.prix_vente_ttc_num = parseFloat(article.prix_vente_ttc) 1140 article.prix_vente_ht_num = parseFloat(article.prix_vente_ht) 1141 article.prix_vente_ttc_min_num = parseFloat(article.prix_vente_ttc_min) 1142 article.prix_vente_ht_min_num = parseFloat(article.prix_vente_ht_min) 1143 1144 // Formattage des prix pour l'affichage 1145 article.prix_vente_ttc = formatPrice(article.prix_vente_ttc) 1146 article.prix_vente_ht = formatPrice(article.prix_vente_ht) 1147 article.prix_vente_multiples = article.prix_vente_multiples || false 1148 article.prix_vente_ttc_min = formatPrice(article.prix_vente_ttc_min) 1149 article.prix_vente_ht_min = formatPrice(article.prix_vente_ht_min) 1150 1151 // Promotions 1152 article.promotions_prix = article.promotions_prix || []
1153 article.promotions_prix.forEach(promo => { 1154 promo.pu_ht_brut = formatPrice(promo.pu_ht_brut) 1155 promo.pu_ttc_brut = formatPrice(promo.pu_ttc_brut) 1156 }) 1157 1158 // Divers 1159 article.en_stock = article.en_stock || false 1160 article.lib_article = article.lib_article || "" 1161 article.url_images = article.url_images || [] 1162 article.id_spv = article.id_spv || [] 1163 article.en_stock = article.en_stock || false 1164 article.qte_in_stock = article.qte_in_stock || 0 1165 article.vente_hors_stock = article.vente_hors_stock || null 1166 1167 // Workaround article marked as not configurable but because laisons not loaded 1168 if(article.liaisons == null && article.is_configurable != null) { 1169 delete article.is_configurable; 1170 } 1171 1172 return article 1173 }, 1174 // Déclinaisons - Sélection de la déclinaison active par défaut 1175 initDeclinaison() { 1176 let that = this 1177 1178 if(this.article && this.article.info_declinaison && this.article.info_declinaison.declinaisons_list && Array.isArray(this.article.info_declinaison.declinaisons_list)) { 1179 this.article.info_declinaison.declinaisons_list.forEach(decli => { 1180 if(decli.id_article == article.id_article && Array.isArray(decli.criteres_declinaison)) { 1181 decli.criteres_declinaison.forEach(carac => { 1182 that.declinaisons[carac.id_carac] = carac.valeur; 1183 }) 1184 } 1185 }) 1186 } 1187 }, 1188 initLiaisons() { 1189 this.article.prix_vente_min_ht = 0; 1190 this.article.prix_vente_min_ttc = 0; 1191 1192 if (this.article && Array.isArray(this.article.liaisons)) { 1193 this.article.liaisons.forEach(type_liaison => { 1194 if (type_liaison.sens_liaison === "AB") { 1195 type_liaison.articles.forEach(art => { 1196 const processArticle = (key) => { 1197 let hasPositiveStock = false; 1198 let positiveStock = null; 1199 1200 if (art.statut_declinaison == 2) { 1201 art.info_declinaison.declinaisons_list.some(declinaison => { 1202 if (declinaison.qte_stock > 0) { 1203 hasPositiveStock = true; 1204 positiveStock = declinaison.qte_stock; 1205 return true; 1206 } 1207 }); 1208 } else { 1209 hasPositiveStock = art.qte_in_stock > 0; 1210 } 1211 1212 art.hasPositiveStock = hasPositiveStock; 1213 art.positiveStock = positiveStock; 1214 this[key].push(art); 1215 }; 1216 1217 if (art.params.includes("EcomArticleCrossSell")) { 1218 processArticle('effetsEcomArticleCrossSell'); 1219 } 1220 if (art.params.includes("EcomArticleListe")) { 1221 processArticle('effetsEcomArticleListe'); 1222 } 1223 1224 art.points = []; 1225 if (art.params) { 1226 const parsed = JSON.parse(art.params); 1227 if (parsed.EcomProduitVueEclatee && parsed.EcomProduitVueEclatee.points) { 1228 if (parsed.EcomProduitVueEclatee.points.length > 0) { 1229 const extraItems = { 1230 ...art, 1231 qty: 0, 1232 promotions_prix: art.promotions_prix || [], 1233 is_ongoing_promotion: art.is_ongoing_promotion, 1234 programmation_prix: art.programmation_prix, 1235 prix_vente_ttc_num: art.prix_vente_ttc || 0, 1236 prix_vente_ht_num: art.prix_vente_ht || 0, 1237 prix_vente_ttc_min_num: art.prix_vente_ttc_min || 0, 1238 prix_vente_ht_min_num: art.prix_vente_ht_min || 0, 1239 // Formattage des prix pour l'affichage 1240 prix_vente_ttc: art.prix_vente_ttc || 0, 1241 prix_vente_ht: art.prix_vente_ht || 0, 1242 prix_vente_multiples: art.prix_vente_multiples || false, 1243 prix_vente_ttc_min: art.prix_vente_ttc_min || 0, 1244 prix_vente_ht_min: art.prix_vente_ht_min || 0, 1245 }; 1246 1247 art.points = [];
1248 parsed.EcomProduitVueEclatee.points.forEach(point => { 1249 art.points.push({...point, ...extraItems}); 1250 // use for slider 1251 if (point.parentImageName && !this.article.points[point.parentImageName]) { 1252 this.article.points[point.parentImageName] = []; 1253 } 1254 1255 1256 if (this.article.points[point.parentImageName]) { 1257 this.article.points[point.parentImageName].push({...extraItems, ...point, id_article: art.id_article}); 1258 } 1259 1260 if (!this.article.allPointsTemp[art.id_article]) { 1261 this.article.allPointsTemp[art.id_article] = []; 1262 } 1263 1264 if (this.article.allPointsTemp[art.id_article]?.length == 0) { 1265 this.article.allPoints.push({ 1266 ...extraItems, 1267 ...point, 1268 }); 1269 } 1270 1271 let prix_vente_ht_default = 0; 1272 let prix_vente_ttc_default = 0; 1273 let is_ongoing_promotion = false; 1274 1275 if(this.article.id_tarif_client && art['promotions_prix'] && art['promotions_prix'].length > 0) { 1276 const tarif_client = art['promotions_prix'].find(t => t.id_tarif == this.article.id_tarif_client); 1277 if(tarif_client) { 1278 prix_vente_ht_default = tarif_client.pu_ht_brut; 1279 prix_vente_ttc_default = tarif_client.pu_ttc_brut; 1280 } 1281 is_ongoing_promotion = this.isOngoingPromotion(tarif_client); 1282 } 1283 1284 this.article.allPointsTemp[art.id_article].push({ 1285 ...extraItems, 1286 ...point, 1287 prix_vente_ht_default, 1288 prix_vente_ttc_default, 1289 is_ongoing_promotion, 1290 }); 1291 }); 1292 1293 } 1294 } 1295 } 1296 }); 1297 } 1298 type_liaison.articles.forEach(art => { 1299 if (art.params.includes("EcomArticleListe") && (art.prix_vente_ht > 0 && (art.prix_vente_ht < this.article.prix_vente_min_ht || this.article.prix_vente_min_ht === 0))) { 1300 this.article.prix_vente_min_ht = art.prix_vente_ht; 1301 this.article.prix_vente_min_ttc = art.prix_vente_ttc 1302 } 1303 }); 1304 1305 }); 1306 1307 } 1308 }, 1309 getCommonParentPersonnalisations() { 1310 this.article.personnalisationsParentOptions = []; 1311 if (Array.isArray(this.article.options)) { 1312 this.article.personnalisationsParentOptions = this.article.options 1313 .filter(carac => carac.objet_cible === 'option_personnalisation_vente') 1314 .map(carac => { 1315 return { 1316 values: {}, 1317 ...carac, 1318 label: carac.lib_carac, 1319 } 1320 }); 1321 } 1322 }, 1323 /** 1324 * Fonction utilisée pour afficher les options de déclinaisons inexistantes en gris 1325 * Pour un sélecteur de carac (id_carac) et une valeur (value) donnés, renvoie true si cette valeur est 1326 * possible. Se base sur les valeurs des autres sélecteurs. 1327 * 1328 * Par ex si on a un article dispo en Rouge S, Rouge M, Bleu S mais PAS Bleu M et que le premier sélecteur 1329 * est Bleu, alors cette fonction doit renvoyer true pour S mais false pour M. Si on passe le premier 1330 * sélecteur sur Rouge alors elle doit renvoyer true pour S et M. 1331 * 1332 * @param int id_carac - L'id de la carac de déclinaison concernée par le sélecteur 1333 * @param int|string value - La valeur de l'option 1334 * @param boolean is_select - Si true, c'est le sélecteur qui appelle la fonction 1335 */ 1336 declinaisonValueExists(id_carac, value, is_select = false) { 1337 let exists = false 1338 1339 // Si c'est le sélecteur qui appelle la fonction et que la valeur est vide, on ne le grise pas 1340 // car sinon tous les sélecteurs vides par défaut sont grisés et ça fait moche 1341 if(!value && is_select) { 1342 return true 1343 } 1344 1345 if(this.article?.info_declinaison && this.article.info_declinaison?.declinaisons_list && Array.isArray(this.article.info_declinaison.declinaisons_list)) { 1346 this.article.info_declinaison.declinaisons_list.forEach(decli => { 1347 if(!decli?.criteres_declinaison) { 1348 return 1349 } 1350 1351 let is_ok = true 1352 // Pour chaque décli on regarde si ces valeurs correspondent à celles actuellement sélectionnées 1353 decli.criteres_declinaison.forEach(carac => { 1354 // On saute la carac en cours 1355 if(carac.id_carac == id_carac) { 1356 // On ignore les déclinaisons dont la valeur de cette carac n'est pas la bonne 1357 if(carac.valeur != value) { 1358 is_ok = false 1359 } 1360 return; 1361 } 1362 1363 if(this.declinaisons[carac.id_carac] && this.declinaisons[carac.id_carac] != carac.valeur) { 1364 is_ok = false 1365 } 1366 }) 1367 if(is_ok) { 1368 exists = true 1369 } 1370 }) 1371 } 1372 return exists 1373 }, 1374 articleRelativeHandle() { 1375 const selectedVariant = []; 1376 this.article?.info_declinaison?.caracs_valeurs_list.forEach((item) => { 1377 const selectedValue = this.declinaisons[item.id_carac] ?? ''; 1378 selectedVariant.push({ id_carac: `${item.id_carac}`, valeur: selectedValue }); 1379 }); 1380 1381 const id_articles = this.article?.info_declinaison?.declinaisons_list.reduce((acc, curr) => { 1382 const match = selectedVariant.every((variant) => 1383 curr.criteres_declinaison.some( 1384 (criteres) => 1385 criteres.id_carac.toString() === variant.id_carac && 1386 criteres.valeur === variant.valeur 1387 ) 1388 ); 1389 if (match) { 1390 let redirectArticle = curr.id_article; 1391 // Récupération du slug si disponible 1392 let articleUrl = curr.url || false; 1393 if(articleUrl) { 1394 let urlParts = articleUrl.split("/"); 1395 if(urlParts?.length) { 1396 redirectArticle = urlParts[urlParts.length - 1] 1397 } 1398 } 1399 1400 acc.push(redirectArticle); 1401 } 1402 1403 return acc; 1404 }, []); 1405 1406 if (id_articles.length === 1) { 1407 this.isChangingDeclinaison = true; 1408 window.location.href = id_articles[0]; 1409 } 1410 }, 1411 getImagePoints(url, index) { 1412 $arr = url.split("/"); 1413 if ($arr[$arr.length - 1]) { 1414 $parentImageName = $arr[$arr.length - 1]; 1415 /* https://docs.google.com/document/d/18WTBg7N0MroiOUDv5JZKMewyK0yS_u3RVyIwIwMvLUU/edit?tab=t.0#bookmark=id.4am1chgeizp5 1416 if (index == 0 && this.article?.points[$parentImageName]?.length) { 1417 this.article.selectedPoint = this.article.points[$parentImageName][0]; 1418 } */ 1419 return this.article.points[$parentImageName] ? this.article.points[$parentImageName] : []; 1420 } 1421 return url; 1422 }, 1423 prepareDataPoints(articles) { 1424 const dataTagsTable = []; 1425 1426 // dataSpareParts vaut null pour les articles sans vue éclatée (fetchData l'écrase avec 1427 // article.data_spare_parts, cf. plus haut) -> déstructurer null jette un TypeError. Fallback {}. 1428 const {dataTags, dataTable, selectedPoint} = articles?.dataSpareParts ?? {}; 1429 1430 const currentImage = articles?.currentImage ? articles?.currentImage : selectedPoint?.parentImageName; 1431 1432 if (dataTags && currentImage) { 1433 let tagsData = dataTags[currentImage] || {}; 1434 1435 Object.keys(dataTable).forEach(key => { 1436 1437 // Get previous quantity if it exists 1438 const existingArticle = articles.allPoints?.find(a => a.id_article == key); 1439 const previousQty = existingArticle ? existingArticle.qty : 0; 1440 1441 let prix_vente_ht_default = 0; 1442 let prix_vente_ttc_default = 0; 1443 let is_ongoing_promotion = false;
1444 if(articles.id_tarif_client && dataTable[key][0] && dataTable[key][0]['promotions_prix'] && dataTable[key][0]['promotions_prix'].length > 0) { 1445 const tarif_client = dataTable[key][0]['promotions_prix'].find(t => t.id_tarif == articles.id_tarif_client); 1446 if(tarif_client) { 1447 prix_vente_ht_default = tarif_client.pu_ht_brut; 1448 prix_vente_ttc_default = tarif_client.pu_ttc_brut; 1449 } 1450 is_ongoing_promotion = this.isOngoingPromotion(tarif_client); 1451 } 1452 1453 let tagData = tagsData[key] ? tagsData[key] : dataTable[key][0]; 1454 1455 // Fix URL missing language prefix 1456 if (window?.drupalSettings?.path?.pathPrefix != undefined && window?.drupalSettings?.path?.pathPrefix !='fr/') { 1457 let pathPrefix = window?.drupalSettings?.path?.pathPrefix; 1458 let metaURL = tagData.meta_url; 1459 let url = new URL(metaURL); 1460 let path = url.pathname; 1461 if (path.indexOf(`/${pathPrefix}`) != 0) { 1462 url.pathname = `/${pathPrefix}${path.substring(1)}`; 1463 tagData.meta_url = url.href; 1464 } 1465 } 1466 dataTagsTable.push({ 1467 qty: previousQty, 1468 ...(tagData), 1469 tags: tagsData[key] ? tagsData[key].tags : [], 1470 prix_vente_ht_default, 1471 prix_vente_ttc_default, 1472 vente_hors_stock: articles.vente_hors_stock ?? 0, 1473 is_ongoing_promotion, 1474 }); 1475 }); 1476 } 1477 1478 // sort by tags 1479 dataTagsTable.sort((a, b) => parseInt(a.tags[0]) - parseInt(b.tags[0])); 1480 1481 return dataTagsTable; 1482 }, 1483 isOngoingPromotion(promotion) { 1484 const now = new Date().getTime(); 1485 const start = new Date(promotion.date_debut).getTime(); 1486 const end = promotion.date_fin ? new Date(promotion.date_fin).getTime() : null; 1487 return start <= now && (end === null || now <= end); 1488 }, 1489 showPointInfo(point) { 1490 if (this.article.allPointsTemp.length > 0 && point) { 1491 this.article.selectedPoint = this.article.allPointsTemp[point.id_article].find(x => x.id_article == point.id_article && x.tag == point.tag); 1492 this.pointX = point.pos_x; 1493 this.pointY = point.pos_y; 1494 } 1495 }, 1496 onImageLoad(index) { 1497 const currentSlide = this.$el.querySelector(`.slick-slide[data-slick-index="${index}"]`); 1498 if (currentSlide) { 1499 const img = currentSlide.querySelector('img'); 1500 if (this.imageDimensions && !this.imageDimensions[index]) { 1501 this.imageDimensions[index] = {}; 1502 } 1503 1504 if (img && img.complete && img.naturalWidth !== 0) { 1505 // Store the dimensions in the map 1506 this.imageDimensions[index] = { width: $('.slick-slide[data-slick-index="'+index+'"] > div > img').width(), height: $('.slick-slide[data-slick-index="'+index+'"] > div > img').height() }; 1507 } 1508 } else { 1509 // For Alpine.js template support 1510 const images = this.$el.querySelectorAll('#carouselImagePrincipale img'); 1511 if (images[index]) { 1512 const img = images[index]; 1513 if (this.imageDimensions && !this.imageDimensions[index]) { 1514 this.imageDimensions[index] = {}; 1515 } 1516 1517 if (img && img.complete && img.naturalWidth !== 0) { 1518 // Store the actual displayed dimensions 1519 this.imageDimensions[index] = { 1520 width: img.offsetWidth, 1521 height: img.offsetHeight 1522 }; 1523 } 1524 } 1525 } 1526 }, 1527 getImageContainerStyle(index) { 1528 // Always try to get real-time dimensions first 1529 this.updateImageDimensions(index); 1530 1531 if (this.imageDimensions && this.imageDimensions[index]) { 1532 return `width: ${this.imageDimensions[index].width}px; height: ${this.imageDimensions[index].height}px; position: absolute;`; 1533 } 1534 return 'position: absolute;'; 1535 }, 1536 updateImageDimensions(index) { 1537 // Get current image dimensions in real-time 1538 const images = this.$el.querySelectorAll('#carouselImagePrincipale img'); 1539 if (images[index]) { 1540 const img = images[index]; 1541 if (!this.imageDimensions) { 1542 this.imageDimensions = []; 1543 } 1544 1545 // Update dimensions with current displayed size 1546 this.imageDimensions[index] = { 1547 width: img.offsetWidth || img.clientWidth, 1548 height: img.offsetHeight || img.clientHeight 1549 }; 1550 } 1551 }, 1552 changeImage(url) { 1553 const arr = url.split("/"); 1554 if (arr[arr.length - 1]) { 1555 this.article.currentImage = arr[arr.length - 1]; 1556 if(!this.article.firstLoading) { 1557 this.article.allPoints = this.prepareDataPoints(this.article); 1558 } 1559 } 1560 1561 this.article.selectedPoint = null; 1562 this.showCaracs = false;
1563 this.showDescription = false; 1564 if (this.$el) { 1565 this.imgWidth = this.$el.naturalWidth ?? 0; 1566 } 1567 }, 1568 closePointInfo() { 1569 this.showCaracs = false; 1570 this.showDescription = false; 1571 this.article.selectedPoint = null; 1572 }, 1573 calculateTotalSum(products_price) { 1574 return this.article?.allPoints?.reduce((total, product) => { 1575 let unitPrice = 0; 1576 1577 if (products_price === 'ttc' || products_price === 'ht_ttc') { 1578 if (product.prix_vente_multiples && product.prix_vente_ttc_min) { 1579 unitPrice = product.prix_vente_ttc_min; 1580 } else if (!product.prix_vente_multiples && product.prix_vente_ttc) { 1581 unitPrice = product.prix_vente_ttc; 1582 } 1583 } 1584 1585 if (products_price === 'ht') { 1586 if (product.prix_vente_multiples && product.prix_vente_ht_min) { 1587 unitPrice = product.prix_vente_ht_min; 1588 } else if (!product.prix_vente_multiples && product.prix_vente_ht) { 1589 unitPrice = product.prix_vente_ht; 1590 } 1591 } 1592 1593 if (products_price === 'ht_ttc') { 1594 if (product.prix_vente_multiples && product.prix_vente_ht_min) { 1595 unitPrice = product.prix_vente_ht_min; 1596 } else if (!product.prix_vente_multiples && product.prix_vente_ht) { 1597 unitPrice = product.prix_vente_ht; 1598 } 1599 } 1600 1601 return total + product.qty * unitPrice; 1602 }, 0); 1603 }, 1604 countProducts() { 1605 return this.article?.allPoints?.reduce((total, product) => { 1606 return total + product.qty; 1607 }, 0); 1608 }, 1609 additionalParams(products_price, article_valuation_options) { 1610 return { 1611 article_valuation_options: article_valuation_options, 1612 products_price: products_price, // Can be 'ttc', 'ht', or 'ht_ttc' 1613 }; 1614 }, 1615 getDeslinaisonCaracValue(carac_item) { 1616 if(typeof this.declinaisons[carac_item.id_carac] === 'undefined') return null; 1617 const idCarac = carac_item.id_carac; 1618 const valeurTarget = this.declinaisons[carac_item.id_carac]; 1619 1620 if (Number(idCarac) === carac_item.id_carac) { 1621 const matchingValue = carac_item.valeurs.find( 1622 item => item.valeur === valeurTarget 1623 ); 1624 1625 if (matchingValue) { 1626 const result = { 1627 id_carac: carac_item.id_carac, 1628 lib_carac: carac_item.lib_carac, 1629 valeur: matchingValue.valeur, 1630 lib_value: matchingValue.lib_value, 1631 }; 1632 1633 if ('marqueur' in matchingValue) { 1634 result.marqueur = matchingValue.marqueur; 1635 } 1636 1637 return result; 1638 } 1639 } 1640 1641 return null; 1642 }, 1643 getAvailableCaracValue(caracSource, caracIndex) { 1644 const declinaisons_list = this.article.info_declinaison?.declinaisons_list || []; 1645 const declinaisons = this.declinaisons || {}; 1646 1647 const targetIdCarac = caracSource.id_carac.toString(); 1648 1649 const searchCriteria = {}; 1650 for (const [key, value] of Object.entries(declinaisons)) { 1651 if (key !== targetIdCarac) { 1652 searchCriteria[key] = value; 1653 } 1654 } 1655 1656 const matchingProducts = declinaisons_list.filter(product => { 1657 for (const [idCarac, value] of Object.entries(searchCriteria)) { 1658 const hasCriteria = product.criteres_declinaison.some( 1659 crit => crit.id_carac.toString() === idCarac && crit.valeur === value 1660 ); 1661 if (!hasCriteria) return false; 1662 } 1663 1664 return true; 1665 }); 1666 1667 const availableValues = matchingProducts 1668 .flatMap(product => { 1669 const criteria = product.criteres_declinaison.filter( 1670 crit => crit.id_carac.toString() === targetIdCarac 1671 ); 1672 return criteria.map(crit => crit.valeur); 1673 }); 1674 1675 const uniqueAvailableValues = [...new Set(availableValues)]; 1676 let isEnable = false; 1677 1678 const valuesWithStatus = caracSource.valeurs.map(valueItem => { 1679 const isAvailable = uniqueAvailableValues.includes(valueItem.valeur); 1680 if(isAvailable) isEnable = true; 1681 return { 1682 ...valueItem, 1683 enable: caracIndex === 0 ? true : isAvailable, 1684 }; 1685 }); 1686 1687 return { 1688 id_carac: caracSource.id_carac, 1689 lib_carac: caracSource.lib_carac, 1690 values: valuesWithStatus, 1691 isEnable, 1692 }; 1693 },
1694 chooseCarac(id_carac, value) { 1695 this.declinaisons[id_carac] = value; 1696 1697 const matchingProduct = this.findMatchingProduct( 1698 this.declinaisons, 1699 id_carac, 1700 value, 1701 this.article?.info_declinaison?.declinaisons_list || [] 1702 ); 1703 if (matchingProduct && matchingProduct.url) { 1704 let redirectArticle = null; 1705 let urlParts = matchingProduct.url.split("/"); 1706 if(urlParts?.length) { 1707 redirectArticle = urlParts[urlParts.length - 1] 1708 } 1709 this.isChangingDeclinaison = true; 1710 window.location.href = redirectArticle; 1711 return true; 1712 } else { 1713 return false; 1714 } 1715 }, 1716 findMatchingProduct(criteria, priorityIdCarac, priorityValue, products) { 1717 if (!products || products.length === 0) { 1718 return null; 1719 } 1720 1721 let priorityMatchingProducts = products; 1722 1723 if (priorityIdCarac && priorityValue) { 1724 priorityMatchingProducts = products.filter(item => { 1725 return item.criteres_declinaison.some(criterion => 1726 criterion.id_carac === priorityIdCarac && 1727 criterion.valeur === priorityValue 1728 ); 1729 }); 1730 1731 if (priorityMatchingProducts.length === 0) { 1732 priorityMatchingProducts = products; 1733 } 1734 } 1735 1736 const exactMatches = priorityMatchingProducts.filter(item => { 1737 return Object.keys(criteria).every(key => { 1738 const keyNum = parseInt(key); 1739 return item.criteres_declinaison.some(criterion => 1740 criterion.id_carac === keyNum && criterion.valeur === criteria[key] 1741 ); 1742 }); 1743 }); 1744 1745 if (exactMatches.length > 0) { 1746 return exactMatches[0]; 1747 } 1748 1749 const allExactMatches = products.filter(item => { 1750 return Object.keys(criteria).every(key => { 1751 const keyNum = parseInt(key); 1752 return item.criteres_declinaison.some(criterion => 1753 criterion.id_carac === keyNum && criterion.valeur === criteria[key] 1754 ); 1755 }); 1756 }); 1757 1758 if (allExactMatches.length > 0) { 1759 return allExactMatches[0]; 1760 } 1761 1762 const scoredProducts = priorityMatchingProducts.map(item => { 1763 let score = 0; 1764 const criteriaMap = {}; 1765 1766 item.criteres_declinaison.forEach(criterion => { 1767 criteriaMap[criterion.id_carac] = criterion.valeur; 1768 }); 1769 1770 1771 Object.keys(criteria).forEach(key => { 1772 const keyNum = parseInt(key); 1773 if (criteriaMap[keyNum] === criteria[key]) { 1774 1775 score += (keyNum === priorityIdCarac) ? 10 : 1; 1776 } 1777 }); 1778 1779 return { 1780 product: item, 1781 score 1782 }; 1783 }); 1784 1785 scoredProducts.sort((a, b) => b.score - a.score); 1786 1787 if (scoredProducts.length > 0 && scoredProducts[0].score > 0) { 1788 return scoredProducts[0].product; 1789 } 1790 1791 const allScoredProducts = products.map(item => { 1792 let score = 0; 1793 const criteriaMap = {}; 1794 1795 item.criteres_declinaison.forEach(criterion => { 1796 criteriaMap[criterion.id_carac] = criterion.valeur; 1797 }); 1798 1799 Object.keys(criteria).forEach(key => { 1800 const keyNum = parseInt(key); 1801 if (criteriaMap[keyNum] === criteria[key]) { 1802 score += (keyNum === priorityIdCarac) ? 10 : 1; 1803 } 1804 }); 1805 1806 return { 1807 product: item, 1808 score 1809 }; 1810 }); 1811 1812 allScoredProducts.sort((a, b) => b.score - a.score); 1813 1814 if (allScoredProducts.length > 0 && allScoredProducts[0].score > 0) { 1815 return allScoredProducts[0].product; 1816 } 1817 1818 return null; 1819 }, 1820 } 1821} 1822 1823function productListHandler(article = {}) { 1824 const messages = new Drupal.Message(); 1825 let selectedCondition = null; 1826 1827 return { 1828 isLoading: false, 1829 article, 1830 cart: { 1831 qty: 1, 1832 linkedProducts: [] 1833 }, 1834 restrict_qty_selection: false, 1835 selectedCondition, 1836 init() { 1837 try { 1838 if(isEmpty(this.article) && this.$el.dataset?.article) { 1839 this.article = JSON.parse(this.$el.dataset.article); 1840 this.setSelectedConditionnement(); 1841 } 1842 } catch (err) { 1843 console.error('Error parsing article data : ', err); 1844 } 1845 }, 1846 setSelectedConditionnement() { 1847 const article = this.article; 1848 if(article.conditionnement?.vente) { 1849 const conditionnementKeys = Object.keys(article.conditionnement?.vente).map(String); 1850 const vente_id = article?.colisage?.types_fav.vente; 1851 // Loop through `colisage_types` and add matching `<option>` elements 1852 if (article?.colisage_types && article?.colisage) { 1853 const colisageArray = Object.values(article.colisage_types); 1854 const validColisages = colisageArray.filter(colisage => 1855 conditionnementKeys.includes(colisage.id_colisage_type.toString()) 1856 ); 1857 let hasFav = false;
1858 colisageArray 1859 .filter(colisage => conditionnementKeys.includes(colisage.id_colisage_type.toString())) // Filter valid items 1860 .forEach(colisage => { 1861 const val = `${colisage.id_colisage_type};${colisage.lib_colisage_type};${article.conditionnement.vente[colisage.id_colisage_type] || 0}`; 1862 if (vente_id == colisage.id_colisage_type) { 1863 this.selectedCondition = val; 1864 hasFav = true; 1865 } 1866 }); 1867 1868 // If no match was found, set to the first valid item 1869 if (!hasFav && validColisages.length > 0) { 1870 const firstColisage = validColisages[0]; 1871 this.selectedCondition = `${firstColisage.id_colisage_type};${firstColisage.lib_colisage_type};${article.conditionnement.vente[firstColisage.id_colisage_type] || 0}`; 1872 } 1873 } 1874 } 1875 }, 1876 isDisabledAddToCart() { 1877 let hasPositiveStock = false; 1878 let positiveStock = null; 1879 1880 this.article?.info_declinaison?.declinaisons_list.forEach((declinaison) => { 1881 if (declinaison.qte_stock > 0 && this.positiveStock === null) { 1882 hasPositiveStock = true; 1883 positiveStock = declinaison.qte_stock; 1884 } 1885 }); 1886 if ( 1887 (this.article?.vente_hors_stock === null || this.article?.vente_hors_stock === 0 || this.article?.is_orderable_art === 0) && 1888 ( 1889 (this.article?.qte_in_stock <= 0 && this.article?.statut_declinaison !== 2) || 1890 (this.article?.statut_declinaison === 2 && !hasPositiveStock) 1891 || this.article?.reappro 1892 ) 1893 ) { 1894 return true; 1895 } 1896 1897 return false; 1898 }, 1899 addToCartHandler(condtionnement = null) { 1900 // On bloque l'ajout au panier, en plus du disabled sur le bouton 1901 // statut_declinaison = $article->getStatut_groupe() | 2: parent, 1: child 1902 if (this.article.statut_declinaison == 2) { 1903 this.isLoading = true; 1904 getProductChild(this.article.id_article).then((res) => { 1905 // const productChilds = res.filter(obj => obj.statut_declinaison === 1 && obj.qte_in_stock > 0); 1906 const productChilds = res.filter(obj => obj.statut_declinaison === 1); 1907 Alpine.store('modal').open({ 1908 id: 'list_product_child', 1909 qty: 1, 1910 productChilds, 1911 }); 1912 this.isLoading = false; 1913 }); 1914 return; 1915 } else { 1916 const nbConditionnements = Object.keys(this.article?.conditionnement?.vente || {}).length; 1917 1918 // On n'ouvre la popup de choix que s'il y a plusieurs conditionnements : l'utilisateur a alors 1919 // un choix à faire. Avec un seul conditionnement (ou aucun), il n'y a rien à choisir, on ajoute 1920 // directement avec ce conditionnement pré-sélectionné. 1921 if (nbConditionnements > 1 && !condtionnement) { 1922 Alpine.store('modal').open({ 1923 id: 'conditionnement', 1924 article: this.article, 1925 selectedCondition: this.selectedCondition 1926 }); 1927 1928 return; 1929 } 1930 1931 // Un seul conditionnement : s'assurer qu'il est sélectionné avant l'ajout direct (selectedCondition 1932 // n'est pas initialisé quand l'article est passé en argument plutôt que via le dataset). 1933 if (nbConditionnements === 1 && !condtionnement && !this.selectedCondition) { 1934 this.setSelectedConditionnement(); 1935 } 1936 } 1937 1938 if (this.article.vente_hors_stock == null || this.article.vente_hors_stock == 0 || this.article.is_orderable_art == 0) { 1939 if (this.article.type != 'service' && this.article.qte_in_stock <= 0 && !this.article.en_stock) { 1940 messages.add( 1941 Drupal.t('Cet article n\'est actuellement pas disponible en stock'), 1942 { type: 'error' } 1943 ); 1944 return; 1945 } 1946 } 1947 1948 let option_data_add_cart = { 1949 'qty': 1, 1950 } 1951 pushDataLayer(this.article, "add_to_cart", "catalogue", "Catalogue client", option_data_add_cart) 1952 1953 let data = {
1954 product_id: this.article.id_article, 1955 qty: 1, 1956 details_qte: this.selectedCondition 1957 } 1958 return addToCart(null, data, this.article, this); 1959 }, 1960 addToCartMultipleHandler(articles, conditionnement) { 1961 if (articles.length === 0) { 1962 return; 1963 } 1964 1965 const articlesFilter = articles.filter(article => article.qty > 0); 1966 1967 const products = [ 1968 ...articlesFilter.map(product => { 1969 if (product.vente_hors_stock == null || product.vente_hors_stock === 0 || product.is_orderable_art === 0) { 1970 if (!product?.qte_in_stock) { 1971 messages.add( 1972 Drupal.t('Attention, l\'article "%lib" n\'est plus en stock, celui-ci n\'est donc pas ajouté.', { 1973 '%lib': product.lib, 1974 }), 1975 { type: 'warning' } 1976 ); 1977 return null; 1978 } 1979 } 1980 1981 let qty = product.qty || 1; 1982 if (product.vente_hors_stock == null || product.vente_hors_stock === 0 || product.is_orderable_art === 0) { 1983 if (qty > product.qte_in_stock) { 1984 qty = product.qte_in_stock 1985 messages.add( 1986 Drupal.t('Attention, la quantité sélectionnée pour l\'article "%lib" dépasse la quantié en stock, celle-ci a donc été réduire à "%qty".', { 1987 '%lib': product?.lib_article, 1988 '%qty': ` ${qty} `, 1989 }), 1990 { type: 'warning' } 1991 ); 1992 } 1993 } 1994 return { id_article: product.id_article, qty } 1995 }).filter(product => product) 1996 ]; 1997 1998 addProductsToCart(products).then(cart => { 1999 if (cart?.contenu?.length) { 2000 const totalQty = cart.contenu.reduce((count, item) => { 2001 if (products.find(product => product.id_article == item.id_article)) { 2002 count += item.qte; 2003 } 2004 return count 2005 }, 0) 2006 2007 Alpine.store('modal').open({ 2008 id: 'added_to_cart', 2009 qty: totalQty 2010 }); 2011 } 2012 2013 loadCartQty(cart, {forceFetch: true, showModal: true}); 2014 }); 2015 }, 2016 addToCartWithConditionHandler(article, conditionnement) { 2017 if (article.vente_hors_stock == null || article.vente_hors_stock == 0 || article.is_orderable_art == 0) { 2018 if (article.type != 'service' && article.qte_in_stock <= 0 && !article.en_stock) { 2019 messages.add( 2020 Drupal.t('Cet article n\'est actuellement pas disponible en stock'), 2021 { type: 'error' } 2022 ); 2023 return; 2024 } 2025 } 2026 2027 let option_data_add_cart = { 2028 'qty': 1, 2029 } 2030 pushDataLayer(article, "add_to_cart", "catalogue", "Catalogue client", option_data_add_cart) 2031 2032 let data = { 2033 product_id: article.id_article, 2034 qty: 1, 2035 details_qte: conditionnement 2036 } 2037 return addToCart(null, data, article); 2038 }, 2039 updateCartQty(line = this.article, qty = 1) { 2040 if(isNaN(qty)) throw new Error('Quantité invalide'); 2041 qty = parseInt(qty); 2042 if(qty == 0) return false; 2043 2044 if(qty < 0) { 2045 line.qte = line.qte > 1 ? line.qte - 1 : line.qte; 2046 } else { 2047 line.qte += qty; 2048 } 2049 2050 //@TODO: update line qte on response / remove modal refresh 2051 updateCartContent(this.$el, line.qte); 2052 2053 // Force input update / @TODO debug 2054 this.$el.parentNode.querySelector('input.cart-qty').value = line.qte; 2055 }, 2056 addToFavoriteHandler(id_article) { 2057 Alpine.store('modal').open({ 2058 id: 'add_favorite', 2059 id_article 2060 }); 2061 }, 2062 } 2063} 2064 2065function getChildArticleByCaracValue(data, carac_id, carac_value) { 2066 let article = {}; 2067 for (const declinaison of data.info_declinaison.declinaisons_list) { 2068 const matchingCarac = declinaison.criteres_declinaison.find( 2069 carac => carac.id_carac == carac_id && carac.valeur == carac_value 2070 ); 2071 2072 if (matchingCarac) { 2073 article.url = declinaison.url; 2074 article.image_url = declinaison.image_url 2075 return article; 2076 } 2077 } 2078 2079 return article; 2080} 2081 2082async function getProductChild(idArticleParent) { 2083 try { 2084 const response = await fetch(getUrl('productSearchReturnJson?id_article_parent=' + idArticleParent), { 2085 method: 'GET', 2086 headers: { 2087 'Accept': 'application/json', 2088 } 2089 }); 2090 const json = await response.json(); 2091 return json?.products || null; 2092 } catch (error) { 2093 console.error(error); 2094 } 2095} 2096 2097/* 2098 * Page récapitulative du panier 2099 */ 2100function cartStep(step) {
2101 $("#cart-page").attr("data-step", step); 2102} 2103 2104/** 2105 * Décrémente ou incrémente la quantité d'une ligne du panier 2106 * @param {*} elm - Le bouton sur lequel on a cliqué 2107 * @param {boolean} decrease - Si true, on décrémente, sinon on incrémente 2108 * @param {boolean} is_modall - Si true, c'est la popup d'ajout au panier qui fait l'appel 2109 */ 2110function updateCartQty(elm, decrease = false, is_modal = false) { 2111 let selector = is_modal ? ".quantite-popup" : ".cart-counter"; 2112 let counterContainer = $(elm).closest(selector); 2113 let qtyInput = $(counterContainer).find(".cart-qty"); 2114 let qty = $(qtyInput).val() ?? 0; 2115 2116 qty = calcQty(qty, decrease); 2117 updateCartContent(elm, qty, is_modal); 2118} 2119 2120function updateConditionnement(elm, val, is_modal = false) { 2121 let selector = ".cart-item"; 2122 let counterContainer = $(elm).closest(selector); 2123 let qtyInput = $(counterContainer).find(".cart-qty"); 2124 let qty = $(qtyInput).val() ?? 0; 2125 updateCartContent(elm, qty, is_modal, val); 2126} 2127 2128function validateMandat() { 2129 Alpine.store('modal').close(); 2130} 2131 2132 2133function updateMandatLine(ele, name) { 2134 const paymentSelected = $('input[name="paymentMode"]:checked'); 2135 const mandat = paymentSelected.siblings(".module-mandat-info"); 2136 const targetInput = mandat.find(`input[name='${name}']`); 2137 targetInput.val($(ele).val()).attr('value', $(ele).val()); 2138} 2139 2140function changePaymentMode() { 2141 cartAjax('cart/content', 'GET', {}, 'change_payment'); 2142} 2143 2144/** 2145 * Fait un appel AJAX pour mettre à jour la ligne dans le panier 2146 * @param {*} elm - L'élément ayant demandé l'appel AJAX (on remontera jusqu'au container pour l'ID) 2147 * @param {int} qty - La quantité mise à jour 2148 * @param is_modal 2149 * les données Alpine plutô que de faire un reload de l'url /panier 2150 */ 2151function updateCartContent(elm, qty, is_modal = null, conditionnement = null) { 2152 try { 2153 let id_panier = elm?.closest('[data-cart]')?.getAttribute('data-cart'); 2154 if (!id_panier) { 2155 console.error("ID panier non trouvé"); 2156 return; 2157 } 2158 2159 qty = parseInt(qty); 2160 if (!qty || isNaN(qty) || qty < 1) { 2161 const messages = new Drupal.Message(); 2162 messages.add(Drupal.t('Quantité incorrecte'), { type: 'error' }); 2163 elm.value = elm.dataset.lastvalue; 2164 return; 2165 } 2166 2167 // Plafond de commande selon le mode de vente hors stock : 2168 // - mode 0 / forbidden : stock seul (data-max) 2169 // - mode 2 (autorisé si réappro) : stock + réappro (data-max-reappro, calculé côté template 2170 // car article_info du panier ne porte pas le conditionnement) 2171 // - mode 1 illimité : pas de plafond 2172 if (elm.dataset.type != 'service') { 2173 let plafond = null; 2174 if (elm.dataset.ventehorsstock == 2 && elm.dataset.maxReappro !== undefined && elm.dataset.maxReappro !== '') { 2175 plafond = parseInt(elm.dataset.maxReappro); 2176 } else if (elm.dataset.ventehorsstock == 0 || elm.dataset.outOfStockMode == 'forbidden') { 2177 plafond = parseInt(elm.dataset.max); 2178 } 2179 if (plafond !== null && !isNaN(plafond) && qty > plafond) { 2180 const messages = new Drupal.Message(); 2181 messages.add(Drupal.t('Quantité disponible insuffisante'), { type: 'error' }); 2182 elm.value = elm.dataset.lastvalue; 2183 return; 2184 } 2185 } 2186 2187 const id_contenu = elm?.closest('[data-contenu]')?.getAttribute('data-contenu'); 2188 if (!id_contenu) { 2189 console.error("ID contenu non trouvé"); 2190 return; 2191 } 2192 2193 if (is_modal == null) { 2194 is_modal = Alpine.store('modal').instance?.cart?.id_panier == id_panier; 2195 } 2196 2197 if (!conditionnement) { 2198 const qtyInput = elm.parentNode?.querySelector('input.cart-qty'); 2199 if (qtyInput) { 2200 qtyInput.setAttribute('data-lastvalue', qty); 2201 } 2202 } 2203 2204 toggleDisabledQuantityBtn(elm, qty, elm.dataset.max); 2205 cartAjax("cart/content", "PUT", {id_panier, id_contenu, qty, conditionnement}, "qty", '', is_modal); 2206 } catch (error) { 2207 console.error('updateCartContent error:', error); 2208 } 2209} 2210 2211/** 2212 * toggle between enabled and disabled based on current qty and max qty 2213 * @param {*} elm - element that triggers the event 2214 * @param {int} currentQty - current qty 2215 * @param {int} maxQty - max qty 2216 * @return Void 2217 */ 2218function toggleDisabledQuantityBtn(elm, currentQty, maxQty) 2219{ 2220 if(isNaN(maxQty)) return false;
2221 maxQty = parseInt(maxQty); 2222 const venteHorsStock = elm.dataset?.ventehorsstock || elm.parentNode?.querySelector('input.cart-qty')?.dataset?.ventehorsstock; 2223 2224 //enabled and disabled reduce qte button 2225 const reduceBtn = elm.parentNode?.querySelector('button.reduceQteBtn'); 2226 if (reduceBtn) { 2227 if(currentQty <= 1){ 2228 reduceBtn.setAttribute('disabled', true); 2229 }else{ 2230 reduceBtn.removeAttribute('disabled'); 2231 } 2232 } 2233 2234 //enabled and disabled raise qte button 2235 const raiseBtn = elm.parentNode?.querySelector('button.raiseQteBtn'); 2236 if (raiseBtn) { 2237 if(currentQty >= maxQty && venteHorsStock != 1){ 2238 raiseBtn.setAttribute('disabled', true); 2239 }else{ 2240 raiseBtn.removeAttribute('disabled'); 2241 } 2242 } 2243} 2244 2245/** 2246 * Fait un appel AJAX pour supprimer une ligne dans le panier 2247 * @param {*} elm - L'élément ayant demandé l'appel AJAX (on remontera jusqu'au container pour l'ID) 2248 */ 2249function removeCartContent(elm) { 2250 const id_panier = $("#cart-page").attr("data-cart"); 2251 if (!id_panier) { 2252 console.error("ID panier non trouvé"); 2253 return; 2254 } 2255 const $elements = $(elm); 2256 const ids_contenu = []; 2257 2258 $elements.each(function () { 2259 let $el = $(this); 2260 let contentContainer = $el.closest(".cart-item"); 2261 if (contentContainer.length === 0) { 2262 contentContainer = $el.closest(".cart-item-no-css"); 2263 } 2264 2265 const id_contenu = contentContainer.attr("data-contenu"); 2266 const articleElement = contentContainer.find("[data-article]").get(0); 2267 if (id_contenu) { 2268 ids_contenu.push(id_contenu); 2269 2270 let dataArticle = articleElement.getAttribute("data-article"); 2271 dataArticle = htmlDecode(dataArticle); 2272 if (dataArticle) { 2273 try { 2274 dataArticle = JSON.parse(dataArticle); 2275 pushDataLayer(dataArticle, "remove_from_cart", "view_cart", "View cart"); 2276 } catch (err) { 2277 console.error(err); 2278 } 2279 } 2280 } 2281 }); 2282 2283 let data = { 2284 id_panier: id_panier, 2285 id_contenu: ids_contenu, 2286 }; 2287 2288 cartAjax("cart/content", "DELETE", data, "delete"); 2289} 2290 2291/** 2292 * Modifie le mode de livraison sélectionné 2293 * @param {*} elm - L'élément ayant demandé l'appel AJAX (on remontera jusqu'au container pour l'ID) 2294 */ 2295function updateShippingMode(elm) {
2296 let id_panier = $("#cart-page").attr("data-cart"); 2297 if (!id_panier) { 2298 console.error("ID panier non trouvé"); 2299 return; 2300 } 2301 2302 let id_livraison_mode = $(elm).val(); 2303 2304 if (!id_livraison_mode) { 2305 console.error("ID mode de livraison non trouvé"); 2306 return; 2307 } 2308 2309 let data = { 2310 id_panier: id_panier, 2311 id_livraison_mode: id_livraison_mode, 2312 }; 2313 2314 cartAjax("cart/shipping", "PUT", data, "shipping"); 2315} 2316 2317function updateMultiLivaraisonMode(livraison_mode) { 2318 let id_panier = $("#cart-page").attr("data-cart"); 2319 if (!id_panier) { 2320 console.error("ID panier non trouvé"); 2321 return; 2322 } 2323 2324 let data = { 2325 id_panier: id_panier, 2326 livraison_mode: 'wait_all_available', 2327 action: 'multi_livraison_mode' 2328 }; 2329 2330 if (livraison_mode === 'ship_separately') { 2331 data.livraison_mode = 'ship_separately'; 2332 } 2333 2334 cartAjax("cart/content", "PUT", data, ""); 2335} 2336 2337function getPointRelais(elm, code_pr = null) { 2338 $("#cart-page > .cart, #cart-page > .cart-content").addClass("loading"); 2339 2340 let id_panier = $("#cart-page").attr("data-cart");
2341 let code_postal = $("#cart-page").attr("data-livraison-code-postal"); 2342 let ville = $("#cart-page").attr("data-livraison-ville"); 2343 let code_pays = $("#cart-page").attr("data-livraison-code_pays"); 2344 let ref_transporteur = $(elm).data("ref_transporteur") 2345 let delai_jours = $(elm).data("delai_jours") 2346 if (!id_panier) { 2347 console.error("ID panier non trouvé"); 2348 return; 2349 } 2350 if (!code_postal) { 2351 console.error("Code postal non trouvé"); 2352 return; 2353 } 2354 2355 let id_livraison_mode = $(elm).val(); 2356 2357 if (!id_livraison_mode) { 2358 console.error("ID mode de livraison non trouvé"); 2359 return; 2360 } 2361 2362 const params = { 2363 id_panier: id_panier, 2364 id_livraison_mode: id_livraison_mode, 2365 code_postal: code_postal, 2366 id_transport_config: $(elm).data("id_transport_config") 2367 }; 2368 2369 let id; 2370 switch (ref_transporteur) { 2371 case "chronopost": 2372 id = "point_relais_chronopost_modal"; 2373 params.ville = ville; 2374 params.code_pays = code_pays; 2375 params.delai_jours = delai_jours; 2376 break; 2377 case "tnt_express_france": 2378 id = "point_relais_tnt_modal"; 2379 break; 2380 case "colissimo_soap": 2381 params.ville = ville; 2382 params.code_pays = code_pays; 2383 id = "point_relais_colissimo_soap_modal"; 2384 break; 2385 case "dpd": 2386 params.ville = ville; 2387 params.code_pays = code_pays; 2388 params.delai_jours = delai_jours; 2389 id = "point_relais_dpd_modal"; 2390 break; 2391 case "mondial_relay_soap": 2392 params.code_pays = code_pays; 2393 id = "point_relais_mondial_relay_soap_modal"; 2394 break; 2395 } 2396 2397 const messages = new Drupal.Message() 2398 const queryParams = new URLSearchParams(params).toString(); 2399 const url = getUrl('cart/point_relais'); 2400 fetch(`${url}?${queryParams}`, { 2401 method: 'GET', 2402 headers: { 2403 'Accept': 'application/json', 2404 }, 2405 }) 2406 .then(response => { 2407 if (!response.ok) { 2408 throw new Error('Network response was not ok'); 2409 } 2410 return response.json(); 2411 }) 2412 .then(data => { 2413 $("#cart-page > .cart, #cart-page > .cart-content").removeClass("loading"); 2414 if (!data.status) { 2415 messages.add( 2416 Drupal.t('Erreur lors de la recherche des point relais'), 2417 { type: 'error' } 2418 ); 2419 return null; 2420 } else if (data.point_relais && !data.point_relais.length) { 2421 messages.add( 2422 Drupal.t('Point relais non trouvé'), 2423 { type: 'warning' } 2424 ); 2425 return null; 2426 } 2427 2428 Alpine.store('modal').open({ 2429 id: id, 2430 point_relais: data.point_relais, 2431 id_livraison_mode: data.id_livraison_mode, 2432 id_panier: data.id_panier, 2433 ref_transporteur: ref_transporteur, 2434 code_pr: code_pr, 2435 }); 2436 }) 2437 .catch(error => { 2438 console.error('Fetch error:', error); 2439 }); 2440} 2441 2442function handlePickupSlotMagasin() { 2443 2444 return { 2445 showSelectedPickupSlotTextSerVer: true, 2446 magasin_selected: null, 2447 list_update_contenu: [], 2448 magasins_type_enlevement: [], 2449 all_magasin: [], 2450 use_timeslot: null, 2451 selectedPickupSlotText: null, 2452 showEditMagasin: false, 2453 showMissingProduct: false, 2454 async fetchMagasinsTypeEnlevement(id_panier, id_magasin, id_livraison_mode) { 2455 this.magasin_selected = id_magasin; 2456 const data = await this.getMagasinsTypeEnlevement(id_panier); 2457 let magasins = data?.magasins_type_enlevement?.magasins; 2458 this.use_timeslot = data?.magasins_type_enlevement?.use_timeslot 2459 2460 if (this.use_timeslot == 1) { 2461 magasins = magasins.filter(magasin => magasin.available_pickup_slots?.length > 0); 2462 } 2463 2464 if (magasins.length > 5) { // if more than 5 magasins, show them on popup 2465 if (localStorage.getItem('showListMagasin')) { 2466 Alpine.store('modal').open({ 2467 id: "list_magasin_type_enlevement", 2468 list_magasin_type_enlevement: magasins, 2469 magasin_count: magasins.length, 2470 id_livraison_mode: id_livraison_mode, 2471 }); 2472 2473 localStorage.removeItem('showListMagasin'); 2474 } 2475 2476 this.showEditMagasin = true; 2477 this.all_magasin = magasins; 2478 this.magasins_type_enlevement = magasins.filter(item => item.id_magasin == id_magasin); 2479 2480 if (localStorage.getItem('showMissingProduct')) { 2481 Alpine.store('modal').open({ 2482 id: "list_missing_products_magasin", 2483 missing_products: this?.magasins_type_enlevement[0]?.missing_products, 2484 id_livraison_mode: id_livraison_mode, 2485 reopen_modal_magasin: true, 2486 all_magasins: magasins, 2487 }); 2488 2489 localStorage.removeItem('showMissingProduct'); 2490 } 2491 2492 } else { 2493 this.magasins_type_enlevement = magasins; // if no, show below the checkbox 2494 } 2495 },
2496 chooseMagasinFromList(magasin, id_livraison_mode, magasin_selected) { 2497 if (magasin_selected == magasin.id_magasin) return; 2498 2499 Alpine.store('modal').close(); 2500 2501 let id_panier = $("#cart-page").attr("data-cart"); 2502 if (!id_panier) { 2503 console.error("ID panier non trouvé"); 2504 return; 2505 } 2506 2507 if (!id_livraison_mode) { 2508 console.error("ID mode de livraison non trouvé"); 2509 return; 2510 } 2511 2512 let data = { 2513 id_panier: id_panier, 2514 id_livraison_mode: id_livraison_mode, 2515 id_magasin: magasin.id_magasin, 2516 }; 2517 2518 if (magasin.has_stock === 0) { 2519 this.showMissingProduct = true; 2520 localStorage.setItem('showMissingProduct', JSON.stringify(this.showMissingProduct)); 2521 } 2522 2523 cartAjax("cart/shipping", "PUT", data, "shipping"); 2524 }, 2525 openModalListMagasin(magasins, id_magasin, id_livraison_mode) { 2526 Alpine.store('modal').open({ 2527 id: "list_magasin_type_enlevement", 2528 list_magasin_type_enlevement: magasins, 2529 magasin_count: magasins.length, 2530 id_livraison_mode: id_livraison_mode, 2531 magasin_selected: id_magasin, 2532 }); 2533 }, 2534 2535 getClassPickupSlot(pickup_slot, reservation) { 2536 let classes = ['magasins__pickup--option']; 2537 2538 const pickup_slot_text = `${pickup_slot.type}--${pickup_slot.id_creneau}--${pickup_slot.date_reservation}` 2539 const reservation_text = `${reservation.type}--${reservation.id_creneau}--${reservation.date_reservation}` 2540 2541 if (pickup_slot_text === reservation_text) { 2542 classes.push('selection'); 2543 } 2544 return classes.join(' '); 2545 }, 2546 choosePickupSlot(id_magasin, pickup_slot, id_livraison_mode) { 2547 this.selectedPickupSlotText = this.getSelectedPickupSlotText(pickup_slot); 2548
2549 let id_panier = $("#cart-page").attr("data-cart"); 2550 if (!id_panier) { 2551 console.error("ID panier non trouvé"); 2552 return; 2553 } 2554 2555 if (!id_livraison_mode) { 2556 console.error("ID mode de livraison non trouvé"); 2557 return; 2558 } 2559 2560 const data = { 2561 id_panier: id_panier, 2562 id_livraison_mode: id_livraison_mode, 2563 id_magasin: id_magasin, 2564 type_reservation: pickup_slot.type, 2565 id_creneau: pickup_slot.id_creneau, 2566 date_reservation: pickup_slot.date_reservation, 2567 selected_pickup_slot_text: this.selectedPickupSlotText, 2568 is_pickup_slot: true, 2569 }; 2570 2571 let url = getUrl("cart/shipping"); 2572 let method = "PUT"; 2573 2574 $.ajax({ method, url, data }).done(function (response) { 2575 let value = "success"; 2576 let display_error = false; 2577 2578 if (typeof response.status == "undefined" || !response.status) { 2579 console.error("Une erreur est survenue"); 2580 value = "error"; 2581 display_error = true; 2582 } 2583 2584 displayMessages(response, display_error); 2585 }); 2586 }, 2587 getSelectedPickupSlotText(pickup_slot) { 2588 const translated_pickup_from = Drupal.t('de'); 2589 const translated_pickup_to = Drupal.t('à '); 2590 return `${pickup_slot.days_of_the_week} ${pickup_slot.date_reservation_format_DDMMYYYY} ${translated_pickup_from} ${pickup_slot.start_time_format_HHii} ${translated_pickup_to} ${pickup_slot.end_time_format_HHii}`; 2591 }, 2592 chooseMagasin(magasin, id_livraison_mode) { 2593 Alpine.store('modal').close(); 2594 2595 this.selectedPickupSlotText = ''; 2596 this.showSelectedPickupSlotTextSerVer = false;
2597 this.magasin_selected = magasin.id_magasin; 2598 2599 let id_panier = $("#cart-page").attr("data-cart"); 2600 if (!id_panier) { 2601 console.error("ID panier non trouvé"); 2602 return; 2603 } 2604 if (!id_livraison_mode) { 2605 console.error("ID mode de livraison non trouvé"); 2606 return; 2607 } 2608 2609 let data = { 2610 id_panier: id_panier, 2611 id_livraison_mode: id_livraison_mode, 2612 id_magasin: this.magasin_selected, 2613 }; 2614 2615 let url = getUrl("cart/shipping"); 2616 let method = "PUT"; 2617 2618 $.ajax({ method, url, data }).done(function (response) { 2619 let value = "success"; 2620 let display_error = false; 2621 2622 if (typeof response.status == "undefined" || !response.status) { 2623 console.error("Une erreur est survenue"); 2624 value = "error"; 2625 display_error = true; 2626 } else { 2627 if (magasin.has_stock === 0) { 2628 Alpine.store('modal').open({ 2629 id: "list_missing_products_magasin", 2630 missing_products: magasin.missing_products, 2631 id_livraison_mode: id_livraison_mode, 2632 }); 2633 } 2634 } 2635 2636 displayMessages(response, display_error); 2637 }); 2638 }, 2639 unChooseMagasin(id_livraison_mode, reopen_modal_magasin, all_magasins) {
2640 let id_panier = $("#cart-page").attr("data-cart"); 2641 if (!id_panier) { 2642 console.error("ID panier non trouvé"); 2643 return; 2644 } 2645 if (!id_livraison_mode) { 2646 console.error("ID mode de livraison non trouvé"); 2647 return; 2648 } 2649 2650 let data = { 2651 id_panier: id_panier, 2652 id_livraison_mode: id_livraison_mode, 2653 delete_id_magasin: true, 2654 }; 2655 2656 let url = getUrl("cart/shipping"); 2657 let method = "PUT"; 2658 2659 $.ajax({ method, url, data }).done(function (response) { 2660 let value = "success"; 2661 let display_error = false; 2662 2663 if (typeof response.status == "undefined" || !response.status) { 2664 console.error("Une erreur est survenue"); 2665 value = "error"; 2666 display_error = true; 2667 } 2668 2669 displayMessages(response, display_error); 2670 }); 2671 2672 document.querySelector('input[name="radio_magasin"]:checked').checked = false; 2673 $('.magasins__pickup, .magasins__address, .magasins__detail').hide() 2674 Alpine.store('modal').close(); 2675 2676 if (reopen_modal_magasin) { 2677 $('#magagsin_info').hide() 2678 Alpine.store('modal').open({ 2679 id: "list_magasin_type_enlevement", 2680 list_magasin_type_enlevement: all_magasins, 2681 magasin_count: all_magasins.length, 2682 id_livraison_mode: id_livraison_mode, 2683 }); 2684 } 2685 }, 2686 showMagasinInfo(magasin) { 2687 Alpine.store('modal').open({ 2688 id: "magasin_info", 2689 magasin: magasin, 2690 }); 2691 }, 2692 formatMagasinTime(heure, min) { 2693 return `${('0' + heure).slice(-2)}:${('0' + min).slice(-2)}`; 2694 }, 2695 formatMagasinPlages(plages) { 2696 const separation = Drupal.t('et'); 2697 return plages.map(plage => { 2698 return `${this.formatMagasinTime(plage.heure_debut, plage.min_debut)} - ${this.formatMagasinTime(plage.heure_fin, plage.min_fin)}`; 2699 }).join(` ${separation} `); 2700 }, 2701 clearMissingProducts() {
2702 const id_panier = $("#cart-page").attr("data-cart"); 2703 2704 if (!id_panier || !this.list_update_contenu) { 2705 console.error("Missing params contenu"); 2706 return; 2707 } 2708 2709 const data = { 2710 id_panier: id_panier, 2711 list_update_contenu: this.list_update_contenu, 2712 }; 2713 2714 Alpine.store('modal').close(); 2715 2716 cartAjax("cart/clear-missing-products", "POST", data, ""); 2717 }, 2718 getMagasinsTypeEnlevement(id_panier) { 2719 return new Promise((resolve, reject) => { 2720 if (!id_panier) { 2721 console.error("ID panier non trouvé"); 2722 resolve(null); 2723 return; 2724 } 2725 2726 const params = { id_panier }; 2727 const messages = new Drupal.Message(); 2728 const queryParams = new URLSearchParams(params).toString(); 2729 const url = getUrl('cart/magasins_type_enlevement'); 2730 2731 fetch(`${url}?${queryParams}`, { 2732 method: 'GET', 2733 headers: { 'Accept': 'application/json' }, 2734 }) 2735 .then(response => { 2736 if (!response.ok) { 2737 throw new Error('Network response was not ok'); 2738 } 2739 return response.json(); 2740 }) 2741 .then(data => { 2742 $("#cart-page > .cart, #cart-page > .cart-content").removeClass("loading"); 2743 if (!data.status) { 2744 messages.add( 2745 Drupal.t('Erreur lors de la recherche des magasins'), 2746 { type: 'error' } 2747 ); 2748 resolve(null); 2749 } else if (data?.magasins_type_enlevement?.magasins && !data?.magasins_type_enlevement?.magasins?.length) { 2750 messages.add( 2751 Drupal.t('Magasins non trouvé'), 2752 { type: 'warning' } 2753 ); 2754 resolve(null); 2755 } else { 2756 resolve(data); 2757 } 2758 }) 2759 .catch(error => { 2760 console.error('Fetch error:', error); 2761 reject(error); 2762 }); 2763 }); 2764 } 2765 }; 2766} 2767 2768function horairesTNTSort(horaires) 2769{ 2770 const sortedDays = { 2771 monday: horaires.monday, 2772 tuesday: horaires.tuesday, 2773 wednesday: horaires.wednesday, 2774 thursday: horaires.thursday, 2775 friday: horaires.friday, 2776 saturday: horaires.saturday, 2777 sunday: horaires.sunday 2778 }; 2779 2780 return sortedDays; 2781} 2782 2783function horairesChronopostSort(horaires) 2784{ 2785 horaires.sort((a, b) => { 2786 const jourA = parseInt(a.jour); 2787 const jourB = parseInt(b.jour); 2788 2789 return jourA - jourB; 2790 }); 2791 2792 return horaires; 2793} 2794 2795// Handle livraison type point relai avec carte 2796function handlePointRelaiModal() { 2797 return { 2798 id_point_relais: null, 2799 id_panier: null, 2800 id_livraison_mode: null, 2801 is_selected: false, 2802 userReallySelected: false, 2803 selectedPointIndex: null, 2804 selectedPointRelai: null, 2805 map: null, 2806 markers: [], 2807 mapInitialized: false, 2808 deferredHighlightIndex: null, 2809 highlightRetryInterval: null, 2810 currentTransporteur: null, 2811 2812 init(modal_id) { 2813 this.$watch( 2814 () => this.$store.modal.instance?.ref_transporteur, 2815 (newTrans, oldTrans) => { 2816 if (newTrans && newTrans !== oldTrans) { 2817 this.resetAll(); 2818 } 2819 }, 2820 { immediate: true } 2821 ); 2822 2823 // Watch modal active 2824 this.$watch(() => Alpine.store('modal').isActive(modal_id), (isActive) => { 2825 if (isActive) { 2826 const ref_transporteur = this.$store.modal.instance?.ref_transporteur; 2827 2828 if (this.currentTransporteur !== ref_transporteur) { 2829 this.resetAll(); 2830 this.currentTransporteur = ref_transporteur; 2831 } 2832 2833 if (this.highlightRetryInterval) clearInterval(this.highlightRetryInterval); 2834 2835 setTimeout(() => { 2836 this.applyPreSelected(); 2837 2838 if (this.mapInitialized) { 2839 this.refreshMapFocus(); 2840 } else { 2841 this.initMap(); 2842 } 2843 2844 // Deferred highlight retry 2845 if (this.deferredHighlightIndex !== null) { 2846 let attempts = 0; 2847 this.highlightRetryInterval = setInterval(() => { 2848 attempts++; 2849 if (this.mapInitialized && this.map && this.markers.length > 0 && this.deferredHighlightIndex !== null) { 2850 this.highlightMarker(this.deferredHighlightIndex); 2851 this.deferredHighlightIndex = null; 2852 clearInterval(this.highlightRetryInterval); 2853 } 2854 if (attempts >= 10) { 2855 clearInterval(this.highlightRetryInterval); 2856 } 2857 }, 300); 2858 } 2859 }, 1200); 2860 } else { 2861 if (this.highlightRetryInterval) clearInterval(this.highlightRetryInterval); 2862 } 2863 }); 2864 }, 2865 2866 resetAll() { 2867 this.resetSelection(); 2868 this.mapInitialized = false;
2869 this.deferredHighlightIndex = null; 2870 this.currentTransporteur = null; 2871 2872 if (this.map) { 2873 this.map.remove(); 2874 this.map = null; 2875 } 2876 this.markers = []; 2877 }, 2878 2879 resetSelection() { 2880 this.selectedPointIndex = null; 2881 this.selectedPointRelai = null; 2882 this.is_selected = false; 2883 this.userReallySelected = false; 2884 this.id_point_relais = null; 2885 this.point_relai_data = null; 2886 this.deferredHighlightIndex = null; 2887 2888 if (this.map && this.markers.length > 0) { 2889 this.markers.forEach(marker => { 2890 marker.setIcon(L.divIcon({ 2891 className: 'custom-div-icon', 2892 html: '<div style="background-color: #ef4444; width: 16px; height: 16px; border-radius: 50%; border: 2px solid white; box-shadow: 0 1px 3px rgba(0,0,0,0.3); cursor: pointer;"></div>', 2893 iconSize: [16, 16], 2894 iconAnchor: [8, 8] 2895 })); 2896 marker.closePopup(); 2897 }); 2898 } 2899 }, 2900 2901 applyPreSelected() { 2902 const pointRelais = this.$store.modal.instance?.point_relais || []; 2903 const code_pr = this.$store.modal.instance?.code_pr; 2904 2905 if (!code_pr || pointRelais.length === 0) { 2906 this.resetSelection(); 2907 return; 2908 } 2909 2910 const index = pointRelais.findIndex(point => 2911 point.identifiant == code_pr || 2912 point.id_point_relais == code_pr || 2913 point.pudoId == code_pr || 2914 point.Num == code_pr 2915 ); 2916 2917 if (index !== -1) { 2918 this.selectedPointIndex = index; 2919 // this.selectedPointRelai = pointRelais[index]; 2920 // this.is_selected = true; 2921 2922 if (this.mapInitialized && this.map && this.markers.length > 0) { 2923 this.highlightMarker(index); 2924 } else { 2925 this.deferredHighlightIndex = index; 2926 } 2927 } else { 2928 this.resetSelection(); 2929 } 2930 }, 2931 2932 hasValidCoordinates() { 2933 const pointRelais = this.$store.modal.instance?.point_relais; 2934 if (!pointRelais || pointRelais.length === 0) return false; 2935 2936 return pointRelais.some(point => { 2937 const coords = this.getCoordinates(point); 2938 return coords.lat && coords.lng && 2939 !isNaN(parseFloat(coords.lat)) && !isNaN(parseFloat(coords.lng)); 2940 }); 2941 }, 2942 2943 // Obtenir les coordonnées selon le transporteur 2944 getCoordinates(point) { 2945 const ref_transporteur = this.$store.modal.instance?.ref_transporteur; 2946 2947 let coords = {}; 2948 2949 switch (ref_transporteur) { 2950 case "chronopost": 2951 case "colissimo_soap": 2952 coords = { 2953 lat: point.coordGeolocalisationLatitude, 2954 lng: point.coordGeolocalisationLongitude 2955 }; 2956 break; 2957 case "tnt_express_france": 2958 coords = { 2959 lat: point.latitude, 2960 lng: point.longitude 2961 }; 2962 break; 2963 case "mondial_relay_soap": 2964 coords = { 2965 lat: parseFloat(point.Latitude.replace(',', '.')), 2966 lng: parseFloat(point.Longitude.replace(',', '.')) 2967 }; 2968 break; 2969 default: 2970 coords = { 2971 lat: point.latitude || point.coordGeolocalisationLatitude, 2972 lng: point.longitude || point.coordGeolocalisationLongitude 2973 }; 2974 break; 2975 } 2976 2977 return coords; 2978 }, 2979 2980 // Obtenir le nom du point relais selon le transporteur 2981 getPointRelaiName(point) { 2982 const ref_transporteur = this.$store.modal.instance?.ref_transporteur; 2983 2984 switch (ref_transporteur) { 2985 case "chronopost": 2986 case "colissimo_soap": 2987 return point.nom || ''; 2988 case "tnt_express_france": 2989 return point.lib_point_relais || '';
2990 case "dpd": 2991 return point.company || ''; 2992 case "mondial_relay_soap": 2993 return point.LgAdr1?.trim() || ''; 2994 default: return point.nom || point.lib_point_relais || ''; 2995 } 2996 }, 2997 2998 // Obtenir l'adresse du point relais selon le transporteur 2999 getPointRelaiAddress(point) { 3000 const ref_transporteur = this.$store.modal.instance?.ref_transporteur; 3001 3002 switch (ref_transporteur) { 3003 case "chronopost": 3004 case "colissimo_soap": 3005 return `${point.adresse1 || ''}, ${point.codePostal || ''} ${point.localite || ''}`; 3006 case "tnt_express_france": 3007 return `${point.adresse || ''}, ${point.code_postal || ''} ${point.ville || ''}`; 3008 case "dpd": 3009 return `${point.houseNo || ''} ${point.street || ''}, ${point.zipCode || ''} ${point.city || ''}`; 3010 case "mondial_relay_soap": 3011 return `${point.LgAdr3}, ${point.CP} ${point.Ville}`; 3012 default: 3013 return point.adresse1 || point.adresse; 3014 } 3015 }, 3016 3017 // Initialiser la carte 3018 initMap() { 3019 if (this.mapInitialized) { 3020 return; 3021 } 3022 3023 if (!this.hasValidCoordinates()) { 3024 return; 3025 } 3026 3027 this.$nextTick(() => { 3028 // Déterminer l'ID du container selon le transporteur 3029 const ref_transporteur = this.$store.modal.instance?.ref_transporteur; 3030 let mapContainerId = 'map-container'; 3031 3032 switch (ref_transporteur) { 3033 case "tnt_express_france": 3034 mapContainerId = 'map-container-tnt'; 3035 break; 3036 case "colissimo_soap": 3037 mapContainerId = 'map-container-colissimo'; 3038 break; 3039 case "chronopost": 3040 mapContainerId = 'map-container-chronopost'; 3041 break; 3042 case "dpd": 3043 mapContainerId = 'map-container-dpd'; 3044 break; 3045 case "mondial_relay_soap": 3046 mapContainerId = 'map-container-mondial_relay_soap'; 3047 break; 3048 default: 3049 mapContainerId = 'map-container'; 3050 break; 3051 } 3052 3053 const mapContainer = document.getElementById(mapContainerId); 3054 if (!mapContainer) { 3055 console.error(`Map container not found. Ensure <div id="${mapContainerId}"> exists in your HTML.`); 3056 return; 3057 } 3058 3059 // Vérifier que Leaflet est disponible (chargé via template) 3060 if (typeof L === 'undefined') { 3061 console.error('Leaflet not available. Please check if it\'s loaded in the template.'); 3062 return; 3063 } 3064 3065 this.createMap(mapContainerId); 3066 }); 3067 }, 3068 3069 3070 3071 // Créer la carte 3072 createMap(mapContainerId = 'map-container') { 3073 const pointRelais = this.$store.modal.instance?.point_relais; 3074 if (!pointRelais || pointRelais.length === 0) { 3075 return; 3076 } 3077 3078 const mapContainer = document.getElementById(mapContainerId); 3079 if (!mapContainer) { 3080 console.info(`Map container not found in createMap: ${mapContainerId}`); 3081 return; 3082 } 3083 3084 const rect = mapContainer.getBoundingClientRect(); 3085 if (rect.width === 0 || rect.height === 0) { 3086 console.warn('Container has no dimensions, setting default size'); 3087 mapContainer.style.height = '400px'; 3088 mapContainer.style.width = '100%'; 3089 } 3090 3091 const refreshMapFocus = () => { 3092 if (!this.map) return; 3093 3094 const code_pr = this.$store.modal.instance?.code_pr; 3095 let preSelectedIndex = null; 3096 let bounds = L.latLngBounds(); 3097 let hasValidMarkers = false; 3098 3099 pointRelais.forEach((point, index) => { 3100 const coords = this.getCoordinates(point); 3101 if (coords.lat && coords.lng && !isNaN(parseFloat(coords.lat)) && !isNaN(parseFloat(coords.lng))) { 3102 if (code_pr && (point.identifiant == code_pr || point.id_point_relais == code_pr || point.pudoId == code_pr)) { 3103 preSelectedIndex = index; 3104 } 3105 bounds.extend([parseFloat(coords.lat), parseFloat(coords.lng)]); 3106 hasValidMarkers = true; 3107 } 3108 }); 3109 3110 this.map.invalidateSize(); 3111 3112 setTimeout(() => { 3113 if (preSelectedIndex !== null && this.selectedPointRelai === null) { 3114 this.selectPointRelai(pointRelais[preSelectedIndex], preSelectedIndex, this.$store.modal.instance?.id_panier, this.$store.modal.instance?.id_livraison_mode); 3115 } else if (hasValidMarkers && bounds.isValid()) { 3116 this.map.fitBounds(bounds, { padding: [40, 40], maxZoom: 15 }); 3117 } else {
3118 this.map.setView([46.2276, 2.2137], 5); 3119 } 3120 }, 200); 3121 }; 3122 3123 if (!mapContainer._leaflet_map) { 3124 try { 3125 if (this.map) this.map.remove(); 3126 this.markers.forEach(marker => marker?.remove()); 3127 this.markers = []; 3128 3129 if (mapContainer._leaflet_id) { 3130 mapContainer.innerHTML = ''; 3131 delete mapContainer._leaflet_id; 3132 } 3133 3134 this.map = L.map(mapContainerId).setView([46.2276, 2.2137], 5); 3135 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 3136 attribution: '© OpenStreetMap contributors' 3137 }).addTo(this.map); 3138 3139 let bounds = L.latLngBounds(); 3140 let hasValidMarkers = false; 3141 let preSelectedIndex = null; 3142 const code_pr = this.$store.modal.instance?.code_pr; 3143 3144 pointRelais.forEach((point, index) => { 3145 const coords = this.getCoordinates(point); 3146 if (coords.lat && coords.lng && !isNaN(parseFloat(coords.lat)) && !isNaN(parseFloat(coords.lng))) { 3147 if (code_pr && (point.identifiant == code_pr || point.id_point_relais == code_pr || point.pudoId == code_pr)) { 3148 preSelectedIndex = index; 3149 } 3150 3151 const defaultIcon = L.divIcon({ 3152 className: 'custom-div-icon', 3153 html: '<div style="background-color: #ef4444; width: 16px; height: 16px; border-radius: 50%; border: 2px solid white; box-shadow: 0 1px 3px rgba(0,0,0,0.3); cursor: pointer;"></div>', 3154 iconSize: [16, 16], 3155 iconAnchor: [8, 8] 3156 }); 3157 3158 const marker = L.marker([parseFloat(coords.lat), parseFloat(coords.lng)], { icon: defaultIcon }) 3159 .addTo(this.map) 3160 .bindPopup(` 3161 <div class="text-sm"> 3162 <strong>${this.getPointRelaiName(point)}</strong><br> 3163 ${this.getPointRelaiAddress(point)} 3164 </div> 3165 `) 3166 .on('click', () => { 3167 this.selectPointRelai(point, index, this.$store.modal.instance?.id_panier, this.$store.modal.instance?.id_livraison_mode); 3168 this.selectListItemByIndex(index); 3169 }); 3170 3171 this.markers.push(marker); 3172 3173 bounds.extend([parseFloat(coords.lat), parseFloat(coords.lng)]); 3174 hasValidMarkers = true; 3175 } 3176 }); 3177 3178 if (!hasValidMarkers) { 3179 this.map.setView([46.2276, 2.2137], 5); 3180 const testMarker = L.marker([46.2276, 2.2137]).addTo(this.map) 3181 .bindPopup('<div class="text-sm"><strong>Aucun point relais trouvé</strong><br>Centre de la France</div>'); 3182 this.markers.push(testMarker); 3183 } 3184 3185 mapContainer._leaflet_map = this.map; 3186 mapContainer._leaflet_markers = this.markers; 3187 this.mapInitialized = true; 3188 3189 refreshMapFocus(); 3190 3191 if (this.deferredHighlightIndex !== null && this.markers[this.deferredHighlightIndex]) { 3192 this.highlightMarker(this.deferredHighlightIndex); 3193 this.deferredHighlightIndex = null; 3194 } 3195 } catch (error) { 3196 console.error('Map creation error:', error); 3197 } 3198 } else if (mapContainer._leaflet_map) { 3199 setTimeout(() => { 3200 refreshMapFocus(); 3201 }, 200); 3202 } 3203 }, 3204 3205 // Sélectionner un point relais (depuis la liste ou la carte) 3206 selectPointRelai(point_relai, index, id_panier, id_livraison_mode, event = null) { 3207 // Retrieve markers from mapContainer rather than this.markers 3208 let mapContainerId = 'map-container'; 3209 3210 const ref_transporteur = this.$store.modal.instance?.ref_transporteur; 3211 switch (ref_transporteur) { 3212 case "tnt_express_france": 3213 mapContainerId = 'map-container-tnt'; 3214 break; 3215 case "colissimo_soap": 3216 mapContainerId = 'map-container-colissimo'; 3217 break; 3218 case "chronopost": 3219 mapContainerId = 'map-container-chronopost'; 3220 break;
3221 case "dpd": 3222 mapContainerId = 'map-container-dpd'; 3223 break; 3224 case "mondial_relay_soap": 3225 mapContainerId = 'map-container-mondial_relay_soap'; 3226 break; 3227 default: 3228 mapContainerId = 'map-container'; 3229 break; 3230 } 3231 3232 const mapContainer = document.getElementById(mapContainerId); 3233 const markers = mapContainer?._leaflet_markers || this.markers; 3234 3235 // Mettre à jour l'index sélectionné 3236 this.selectedPointIndex = index; 3237 this.selectedPointRelai = point_relai; 3238 this.is_selected = true; 3239 this.userReallySelected = true; 3240 if (event) { 3241 const element_clicked = $(event.currentTarget); 3242 const $input = element_clicked.find("input[name='point_relai']"); 3243 $input.prop('checked', true); 3244 } 3245 3246 let id_point_relais; 3247 switch (ref_transporteur) { 3248 case "chronopost": 3249 case "colissimo_soap": 3250 id_point_relais = point_relai.identifiant 3251 break; 3252 case "tnt_express_france": 3253 id_point_relais = point_relai.id_point_relais 3254 break; 3255 case "dpd": 3256 id_point_relais = point_relai.pudoId 3257 break; 3258 case "mondial_relay_soap": 3259 id_point_relais = point_relai.Num 3260 break; 3261 default: 3262 id_point_relais = point_relai.identifiant || point_relai.id_point_relais 3263 break; 3264 } 3265 3266 this.id_point_relais = id_point_relais; 3267 this.id_panier = id_panier; 3268 this.id_livraison_mode = id_livraison_mode; 3269 this.is_selected = true; 3270 3271 this.point_relai_data = { 3272 ...point_relai, 3273 ref_transporteur: ref_transporteur 3274 }; 3275 3276 // Mettre en surbrillance le marqueur sur la carte 3277 this.highlightMarker(index); 3278 }, 3279 3280 // Mettre en surbrillance le marqueur sélectionné et zoom 3281 highlightMarker(selectedIndex) { 3282 // Retrieve markers from mapContainer rather than this.markers 3283 const ref_transporteur = this.$store.modal.instance?.ref_transporteur; 3284 let mapContainerId = 'map-container'; 3285 3286 switch (ref_transporteur) { 3287 case "tnt_express_france": 3288 mapContainerId = 'map-container-tnt'; 3289 break; 3290 case "colissimo_soap": 3291 mapContainerId = 'map-container-colissimo'; 3292 break; 3293 case "chronopost": 3294 mapContainerId = 'map-container-chronopost'; 3295 break; 3296 case "dpd": 3297 mapContainerId = 'map-container-dpd'; 3298 break; 3299 case "mondial_relay_soap": 3300 mapContainerId = 'map-container-mondial_relay_soap'; 3301 break; 3302 default: 3303 mapContainerId = 'map-container'; 3304 break; 3305 } 3306 3307 const mapContainer = document.getElementById(mapContainerId); 3308 const markers = mapContainer?._leaflet_markers || this.markers; 3309 const map = mapContainer?._leaflet_map || this.map; 3310 3311 if (!markers || markers.length === 0 || !map) { 3312 console.warn('No markers or map available for highlight - selectedIndex:', selectedIndex); 3313 return; 3314 } 3315 3316 markers.forEach((marker, idx) => { 3317 if (idx === selectedIndex) { 3318 marker.setIcon(L.divIcon({ 3319 className: 'custom-div-icon selected', 3320 html: '<div style="background-color: #3b82f6; width: 20px; height: 20px; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>', 3321 iconSize: [20, 20], 3322 iconAnchor: [10, 10] 3323 })); 3324 3325 const latlng = marker.getLatLng();
3326 map.setView(latlng, 15, { animate: true, duration: 0.5 }); 3327 marker.openPopup(); 3328 } else { 3329 marker.setIcon(L.divIcon({ 3330 className: 'custom-div-icon', 3331 html: '<div style="background-color: #ef4444; width: 16px; height: 16px; border-radius: 50%; border: 2px solid white; box-shadow: 0 1px 3px rgba(0,0,0,0.3); cursor: pointer;"></div>', 3332 iconSize: [16, 16], 3333 iconAnchor: [8, 8] 3334 })); 3335 } 3336 }); 3337 }, 3338 3339 handleValiderClick() { 3340 if (this.is_selected) { 3341 $("#cart-page > .cart, #cart-page > .cart-content").addClass("loading"); 3342 Alpine.store('modal').close(); 3343 updateShippingPointRelai(this.id_panier, this.id_livraison_mode, this.id_point_relais, this.point_relai_data); 3344 this.is_selected = false; 3345 } 3346 }, 3347 3348 // Zoom out to see all markers 3349 zoomToAllMarkers() { 3350 if (this.map && this.markers.length > 0) { 3351 const bounds = L.latLngBounds(); 3352 this.markers.forEach(marker => bounds.extend(marker.getLatLng())); 3353 3354 if (bounds.isValid()) { 3355 this.map.fitBounds(bounds, { padding: [20, 20], animate: true, duration: 0.5 }); 3356 } 3357 } 3358 }, 3359 3360 zoomToMarker(index) { 3361 if (this.map && this.markers[index]) { 3362 const marker = this.markers[index]; 3363 const latlng = marker.getLatLng(); 3364 this.map.setView(latlng, 15, { animate: true, duration: 0.5 }); 3365 marker.openPopup(); 3366 } 3367 }, 3368 3369 // Select item in list by index 3370 selectListItemByIndex(index) { 3371 const ref_transporteur = this.$store.modal.instance?.ref_transporteur; 3372 let modalId = 'point_relais_chronopost_modal'; 3373 3374 switch (ref_transporteur) { 3375 case "tnt_express_france": 3376 modalId = 'point_relais_tnt_modal'; 3377 break; 3378 case "colissimo_soap": 3379 modalId = 'point_relais_colissimo_soap_modal'; 3380 break; 3381 case "chronopost": 3382 modalId = 'point_relais_chronopost_modal'; 3383 break; 3384 case "dpd": 3385 modalId = 'point_relais_dpd_modal'; 3386 break; 3387 case "mondial_relay_soap": 3388 modalId = 'point_relais_mondial_relay_soap_modal'; 3389 break; 3390 } 3391 3392 // Find the modal container 3393 const modalContainer = document.querySelector(`[data-modal-id="${modalId}"]`) || 3394 document.querySelector(`#${modalId}`) || 3395 document.querySelector('.modal[data-modal="' + modalId + '"]'); 3396 3397 if (!modalContainer) { 3398 console.warn('Modal container not found for:', modalId); 3399 return; 3400 } 3401 3402 // Find all radio buttons in the list 3403 const radioButtons = modalContainer.querySelectorAll('input[name="point_relai"]'); 3404 3405 if (radioButtons && radioButtons[index]) { 3406 // Check the radio button 3407 radioButtons[index].checked = true; 3408 3409 // Trigger change event to ensure any listeners are notified 3410 const event = new Event('change', { bubbles: true }); 3411 radioButtons[index].dispatchEvent(event); 3412 3413 const listItem = radioButtons[index].closest('li, .point-relai-item, [data-point-relai]'); 3414 if (listItem) { 3415 listItem.scrollIntoView({ 3416 behavior: 'smooth', 3417 block: 'nearest' 3418 }); 3419 3420 // Add visual feedback (optional) 3421 listItem.classList.add('selected'); 3422 setTimeout(() => listItem.classList.remove('selected'), 1000); 3423 } 3424 } else { 3425 console.warn('Radio button not found at index:', index); 3426 } 3427 }, 3428 3429 refreshMapFocus() { 3430 if (!this.map) return; 3431 3432 const pointRelais = this.$store.modal.instance?.point_relais || []; 3433 const code_pr = this.$store.modal.instance?.code_pr; 3434 let preSelectedIndex = null; 3435 let bounds = L.latLngBounds(); 3436 let hasValidMarkers = false; 3437 3438 pointRelais.forEach((point, index) => { 3439 const coords = this.getCoordinates(point); 3440 if (coords.lat && coords.lng && !isNaN(parseFloat(coords.lat)) && !isNaN(parseFloat(coords.lng))) { 3441 if (code_pr && (point.identifiant == code_pr || point.id_point_relais == code_pr || point.pudoId == code_pr)) { 3442 preSelectedIndex = index; 3443 } 3444 bounds.extend([parseFloat(coords.lat), parseFloat(coords.lng)]); 3445 hasValidMarkers = true; 3446 } 3447 }); 3448 3449 this.map.invalidateSize(); 3450 3451 setTimeout(() => { 3452 if (preSelectedIndex !== null && this.selectedPointRelai === null) { 3453 this.selectPointRelai(pointRelais[preSelectedIndex], preSelectedIndex, this.$store.modal.instance?.id_panier, this.$store.modal.instance?.id_livraison_mode); 3454 } else if (hasValidMarkers && bounds.isValid()) { 3455 this.map.fitBounds(bounds, { padding: [40, 40], maxZoom: 15 }); 3456 } else {
3457 this.map.setView([46.2276, 2.2137], 5); 3458 } 3459 }, 200); 3460 } 3461 }; 3462} 3463 3464function updateShippingPointRelai(id_panier, id_livraison_mode, id_point_relais, point_relai) { 3465 if (!id_panier) { 3466 console.error("ID panier non trouvé"); 3467 return; 3468 } 3469 3470 if (!id_livraison_mode) { 3471 console.error("ID mode de livraison non trouvé"); 3472 return; 3473 } 3474 3475 let data = { 3476 id_panier: id_panier, 3477 id_livraison_mode: id_livraison_mode, 3478 id_point_relais: id_point_relais, 3479 point_relai: point_relai, 3480 }; 3481 3482 $(currentTransportModeElm).prop("checked", true); 3483 currentTransportModeElm = null; 3484 cartAjax("cart/shipping", "PUT", data, "shipping"); 3485 localStorage.setItem('livraison_point_relais', JSON.stringify(point_relai)) 3486} 3487 3488/** 3489 * Modifie l'adresse sélectionnée 3490 * @param {*} elm - L'élément ayant demandé l'appel AJAX (on remontera jusqu'au container pour l'ID) 3491 */ 3492function updateAddress(elm) { 3493 let id_panier = $("#cart-page").attr("data-cart"); 3494 if (!id_panier) { 3495 console.error("ID panier non trouvé"); 3496 return; 3497 } 3498 3499 let id_adresse = $(elm).val(); 3500 let elm_name = $(elm).attr("name"); 3501 elm_name = elm_name.split("-"); 3502 let type_adresse = elm_name[1]; 3503 3504 if (!id_adresse) { 3505 console.error("ID adresse non trouvé"); 3506 return; 3507 } 3508 3509 if (id_adresse == "selected") { 3510 return false; 3511 } 3512 3513 let data = { 3514 id_panier: id_panier, 3515 id_adresse: id_adresse, 3516 type_adresse: type_adresse, 3517 }; 3518 3519 cartAjax("cart/address", "POST", data, "address"); 3520} 3521 3522/** 3523 * Modifie le commentaire client sur le panier 3524 */ 3525function checkCommentRegex($el, addErrorClass = true) { 3526 const regexStr = $el.data("regex") || ""; 3527 const customMessage = $el.data("error-message") || ""; 3528 const value = $el.val().trim(); 3529 const fieldTitle = $el.closest("div").find("label").text().trim(); 3530 const messages = new Drupal.Message(); 3531 if (addErrorClass) { 3532 $el.removeClass("border-error"); 3533 } 3534 3535 const isRequired = $el.data("required") == "1"; 3536 if (isRequired && value === "") { 3537 if (addErrorClass) { 3538 $el.addClass("border-error"); 3539 } 3540 messages.add( 3541 Drupal.t('Le champ "@title" est obligatoire', { 3542 '@title': fieldTitle 3543 }), 3544 { type: 'error' } 3545 ); 3546 return false; 3547 } 3548 3549 if (!regexStr || value === "") { 3550 return true; 3551 } 3552 3553 let isValid = false; 3554 3555 try { 3556 const fullRegex = new RegExp(regexStr); 3557 isValid = fullRegex.test(value); 3558 } catch (e) { 3559 isValid = false; 3560 } 3561 if (!isValid) { 3562 if (addErrorClass) { 3563 $el.addClass("border-error"); 3564 } 3565 messages.add( 3566 customMessage || 3567 Drupal.t('Le commentaire â@titleâ ne respecte pas le format requis', { 3568 '@title': fieldTitle 3569 }), 3570 { type: 'error' } 3571 ); 3572 return false; 3573 } 3574 return true; 3575} 3576function checkAllCommentRegex() { 3577 let isAllValid = true; 3578 3579 $("textarea[data-regex], textarea[data-required='1']").each(function () { 3580 if (!checkCommentRegex($(this), true)) { 3581 isAllValid = false; 3582 } 3583 }); 3584 return isAllValid; 3585} 3586 3587function updateCartComment(element) { 3588 const $el = $(element); 3589 3590 if (!checkCommentRegex($el, false)) { 3591 return; 3592 } 3593 3594 const infoName = $el.data('info-name') || ""; 3595 const value = $el.val().trim();
3596 const id_panier = $("#cart-page").data("cart"); 3597 3598 if (!id_panier) { 3599 console.error("ID panier non trouvé"); 3600 return; 3601 } 3602 3603 cartAjax("cart/comment", "POST", { 3604 id_panier: id_panier, 3605 titre: infoName, 3606 comment: value 3607 }, "comment"); 3608} 3609/** 3610 * Validation du panier 3611 */ 3612function validateCart(evt) { 3613 evt.preventDefault(); 3614 3615 if (window.__validateCartInFlight) { 3616 return; 3617 } 3618 3619 if (!checkAllCommentRegex()) { 3620 console.warn("Un des commentaires est invalide"); 3621 return; 3622 } 3623 let id_panier = $("#cart-page").attr("data-cart"); 3624 if (!id_panier) { 3625 console.error("ID panier non trouvé"); 3626 return; 3627 } 3628 3629 // Section transport masquée (option hide_shipping_section) : aucun mode n'est présenté au client, 3630 // mais le back exige un mode de livraison. On applique le mode par défaut exposé par le template 3631 // avant de poursuivre la validation. 3632 let defaultShippingMode = $('#default-shipping-mode').val(); 3633 if ($('input[name="shipping-mode"]').length === 0 && defaultShippingMode) { 3634 $.ajax({ 3635 url: getUrl('cart/shipping'), 3636 method: 'PUT', 3637 async: false, 3638 data: { id_panier: id_panier, id_livraison_mode: defaultShippingMode }, 3639 headers: { 'X-Requested-With': 'XMLHttpRequest' } 3640 }); 3641 } 3642 3643 let shippingAvailable = $('input[name="shipping-mode"]'); 3644 let shippingSelected = $('input[name="shipping-mode"]:checked'); 3645 let paymentSelected = $('input[name="paymentMode"]:checked'); 3646 let livraisonSelected = $('input[name="livraison-option"]:checked'); 3647 let comment_client = $('textarea[name="commentary"]').val(); 3648 let floa_params = JSON.parse(htmlDecode(localStorage.getItem('floa_params'))); 3649 let type_payment = paymentSelected.data('id_reglement_type'); 3650 3651 const mandat = paymentSelected.siblings(".module-mandat-info"); 3652 const mandat_info = {}; 3653 const fields = [ 3654 'demander_siret', 3655 'demander_code_department', 3656 'demander_num_dossier_client' 3657 ]; 3658 3659 fields.forEach(name => { 3660 const value = mandat.find(`input[name='${name}']`).val(); 3661 if (value) { 3662 mandat_info[name] = value; 3663 } 3664 }); 3665 3666 const showErrorAndScroll = (errElementId, scrollTargetId) => { 3667 $(`#${errElementId}`).show(); 3668 window.location.href = `#${scrollTargetId}`; 3669 3670 if (window.location.hash === `#${scrollTargetId}`) { 3671 const targetDiv = document.getElementById(scrollTargetId); 3672 if (targetDiv) { // la cible d'erreur peut être absente du DOM (section masquée) 3673 const targetPosition = targetDiv.offsetTop + targetDiv.offsetHeight; 3674 window.scrollTo({ 3675 top: targetPosition, 3676 behavior: "smooth" 3677 }); 3678 } 3679 } 3680 } 3681 let id_shipping_mode = ''; 3682 // Blocage du panier seulement si il y a des modes de livraison dispo et qu'aucun n'est sélectionné 3683 if(shippingAvailable.length > 0) { 3684 if (shippingSelected.length > 0) { 3685 id_shipping_mode = shippingSelected.val(); 3686 } 3687 if (!id_shipping_mode) { 3688 console.error("Mode de livraison non trouvé"); 3689 cartStep("shipping"); 3690 showErrorAndScroll("no-shipping-error", "shipping-mode"); 3691 return; 3692 } 3693 } 3694 $("#no-shipping-error").hide(); 3695 3696 let id_payment_mode = ''; 3697 // Blocage seulement s'il y a des modes de paiement affichés (aligné sur la logique du transport) : 3698 // quand la section paiement est masquée (option hide_payment_section), la validation passe sans mode. 3699 let paymentAvailable = $('input[name="paymentMode"]'); 3700 if (paymentAvailable.length > 0) { 3701 if (paymentSelected.length > 0) { 3702 id_payment_mode = paymentSelected.val(); 3703 } 3704 if (!id_payment_mode) { 3705 console.error("Mode de paiement non trouvé"); 3706 showErrorAndScroll("no-payment-error", "payment-modes"); 3707 return; 3708 } 3709 } 3710 $("#no-payment-error").hide(); 3711 3712 let multiLivraison = ''; 3713 if (livraisonSelected.length > 0) { 3714 multiLivraison = livraisonSelected.val(); 3715 } 3716 3717 let cgv = $("#payment-cgv").is(":checked"); 3718 if (!cgv) { 3719 cartStep("shipping"); 3720 showErrorAndScroll("cgv-error", "payment-cgv"); 3721 return; 3722 } 3723 $("#cgv-error").hide(); 3724 3725 let newFullAddress = getNewAddress(); 3726 3727 let data = { 3728 id_panier: id_panier, 3729 id_payment_mode: id_payment_mode, 3730 multi_livraison: multiLivraison, 3731 useNewAddress: useNewAddress, 3732 newFullAddress: newFullAddress, 3733 comment_client: comment_client, 3734 floa_params: floa_params ?? [], 3735 type_payment: type_payment, 3736 mandat_info: mandat_info 3737 }; 3738
3739 let data_article = $("#cart-page").attr("data-article"); 3740 data_article = JSON.parse(htmlDecode(data_article)); 3741 let data_cart_info = $("#cart-page").attr("data-cart-info"); 3742 data_cart_info = JSON.parse(data_cart_info); 3743 let option_data = { 3744 'currency': data_cart_info.abrev_devise, 3745 } 3746 3747 pushMutilDataLayer(data_article, "begin_checkout", "view_cart", "View cart", option_data) 3748 3749 window.__validateCartInFlight = true; 3750 $(".cart-checkout").prop("disabled", true).attr("aria-disabled", "true"); 3751 3752 cartAjax("cart/validate", "POST", data, "validate"); 3753} 3754 3755function calcQty(qty, decrease = false) { 3756 if (!qty || qty < 1) { 3757 qty = 1; 3758 } 3759 3760 if (decrease) { 3761 qty = qty > 1 ? qty - 1 : qty; 3762 } else { 3763 qty++; 3764 } 3765 3766 return qty; 3767} 3768 3769function calcQtyLiaison(qty, decrease = false) { 3770 if (decrease) { 3771 qty = qty > 0 ? qty - 1 : qty; 3772 } else { 3773 qty++; 3774 } 3775 3776 return qty; 3777} 3778 3779function getNewAddress() { 3780 let address = { 3781 'line1': document.getElementById("add-livraison-adresse")?.value || "", 3782 'line2': '', 3783 'line3': '', 3784 'code_postal': document.getElementById("add-livraison-code")?.value || "", 3785 'ville': document.getElementById("add-livraison-ville")?.value || "", 3786 } 3787 return address; 3788} 3789 3790/** 3791 * Fonction pour l'appel AJAX. Passe le panier en "loading" et recharge la page 3792 * si l'appel se déroule avec succès 3793 * 3794 * @param {string} path - Le chemin à appeler 3795 * @param {string} method - La méthode HTTP (GET / POST / PUT / DELETE) 3796 * @param {object} data - Tableau associatif (JSON) 3797 * @param {string} action - L'action réalisée (utilisé pour l'affichage des messages) 3798 * @param {string} returnUrl - L'URL de redirection (redirige sur la même page avec les actions si nulle) 3799 * @param {boolean} refreshModal - Si true, c'est on refresh juste le contenu de la popup d'ajout 3800 */ 3801function cartAjax(path, method, data, action, returnUrl = null, refreshModal = false) { 3802 // La modification du commentaire ne refresh que le textarea, pas tout le panier 3803 3804 if(action != "comment") { 3805 $("#cart-page > .cart, #cart-page > .cart-content").addClass("loading"); 3806 } 3807 $('#cart-commentary').prop('disabled', true); 3808 3809 let article_data = $("#cart-page").attr("data-article"); 3810 let data_cart_info = $("#cart-page").attr("data-cart-info"); 3811 let selected_payment_mode = $('input[name="paymentMode"]:checked').val(); 3812 3813 try { 3814 if(data_cart_info) { 3815 data_cart_info = JSON.parse(data_cart_info); 3816 let option_data = { 3817 'transaction_id': data.id_panier, 3818 'shipping': data_cart_info.montant_livraison_ht, 3819 'tax': data_cart_info.montant_tva, 3820 'value': data_cart_info.montant_ttc, 3821 'currency': data_cart_info.abrev_devise, 3822 } 3823 localStorage.setItem('article_data', article_data); 3824 localStorage.setItem('option_data', JSON.stringify(option_data)); 3825 } 3826 } catch(err) { 3827 console.error("Erreur lors du stockage des informations sur la transaction", err); 3828 } 3829 3830 let url = getUrl(path); 3831 3832 $.ajax({ method, url, data }).done(function (response) { 3833 let value = "success"; 3834 let display_error = false; 3835 3836 if (typeof response.status == "undefined" || !response.status) { 3837 console.error("Une erreur est survenue"); 3838 value = "error"; 3839 display_error = true; 3840 } 3841 3842 // Pas de maj du panier ou quoi que ce soit si on a juste modifié le commentaire 3843 if (path == "cart/comment") { 3844 $('#cart-commentary').prop('disabled', false); 3845 return; 3846 } 3847 3848 if (path == "cart/validate") { 3849 let pathRedirect = null; 3850 const message = response.message !== null ? encodeURIComponent(response.message) : null; 3851 if (response.status == "pending" || response.status == "success") { 3852 pathRedirect = '/order-confirmation'; 3853 } 3854 if (response.status == "failed") { 3855 pathRedirect = '/panier'; 3856 } 3857 if (pathRedirect !== null && response.id_commande_client !== undefined && response.ref_doc !== undefined) { 3858 pathRedirect += '?id_order=' + response.id_commande_client + '&ref_order=' + response.ref_doc; 3859 if (response.total_ttc !== undefined && response.total_ttc !== '') { 3860 pathRedirect += '&total_ttc=' + encodeURIComponent(response.total_ttc); 3861 } 3862 } 3863 if (pathRedirect !== null && message !== null && message !== undefined && response.display_message === undefined) { 3864 if (response.id_commande_client !== undefined && response.ref_doc !== undefined) { 3865 pathRedirect += '&message_payment=' + message; 3866 } else { 3867 pathRedirect += '?message_payment=' + message; 3868 } 3869 } 3870 if (pathRedirect !== null) { 3871 window.location = pathRedirect; 3872 return; 3873 } 3874 // Ãchec de la validation : on reste sur la page, on libère le verrou double-clic. 3875 window.__validateCartInFlight = false;
3876 $(".cart-checkout").prop("disabled", false).removeAttr("aria-disabled"); 3877 // Validation bloquée côté back (ex. compte existant sur l'e-mail invité) : 3878 // le back renvoie status=false + message (string). On wrap en message 3879 // d'erreur pour qu'il s'affiche en rouge et non comme un simple status. 3880 if (!response.status && response.message && typeof response.message === 'string') { 3881 response.message = { 3882 text: response.message, 3883 type: 'error' 3884 } 3885 } else if (response.error && response.message) { 3886 response.message = { 3887 text: response.message, 3888 type: 'error' 3889 } 3890 } 3891 } 3892 3893 // Si on était dans une modale on la met à jour 3894 if (refreshModal) { 3895 let url = getUrl('cart/content'); 3896 fetch(url, { 3897 method: 'GET', 3898 headers: { 3899 'Accept': 'application/json', 3900 }, 3901 }) 3902 .then(response => response.json()) 3903 .then(json => { 3904 cart = json?.cart || {} 3905 if (Object.keys(cart).length) { 3906 Alpine.store('modal').instance.cart = {...cart}; 3907 Alpine.store('modal').instance.cart.contenu = null; 3908 Alpine.store('modal').instance.qty = null; 3909 Alpine.store('modal').instance.updated = true; 3910 3911 Alpine.nextTick(function () { 3912 Alpine.store('modal').instance.cart.contenu = cart.contenu; 3913 3914 setTimeout(function() { 3915 Alpine.nextTick(function () { 3916 let articleElm = document.querySelector(`.article-panier[data-contenu="${data.id_contenu}"]`); 3917 3918 if (articleElm) { 3919 // Délai pour la transition 3920 setTimeout(function () { 3921 articleElm.scrollIntoView(); 3922 }, 1); 3923 } 3924 }); 3925 }, 1); 3926 }); 3927 } 3928 3929 displayMessages(response, display_error); 3930 }); 3931 } else if (returnUrl) { 3932 window.location = returnUrl; 3933 } else if (response?.redirect_url) { 3934 if (response?.redirection_data) { 3935 submitPaymentPage(response?.redirect_url, response?.redirection_data) 3936 } else { 3937 window.location = response.redirect_url; 3938 } 3939 } else { 3940 // Ajout du queryParam pour les messages 3941 const parser = new URL(window.location); 3942 3943 // Suppression des anciens 3944 let refs = parser.searchParams.keys(); 3945 for (ref of refs) { 3946 parser.searchParams.delete(ref); 3947 } 3948 if (typeof response.action == "undefined" && typeof response.action == "undefined") { 3949 parser.searchParams.set(value, action); 3950 } else { 3951 //Si la réponse contient un message 3952 parser.searchParams.set(value, response.action); 3953 } 3954 if (response.id_contenu_updated) { 3955 parser.searchParams.set("id_contenu_updated", response.id_contenu_updated); 3956 } 3957 if (response.qte_entered) { 3958 parser.searchParams.set("qte_entered", response.qte_entered); 3959 } 3960 3961 // Maj du panier, seulement si le thème le prend en compte 3962 let cartContent = document.querySelector('[data-cart-content]'); 3963 const cartUrl = new URL(getUrl('panier')); 3964 const cartPath = cartUrl.pathname; 3965 const currentPath = window.location.pathname; 3966 const isOnCartPage = currentPath === cartPath || currentPath.startsWith(cartPath); 3967 3968 if(cartContent && isOnCartPage) { 3969 try { 3970 let cartUrl = getUrl('panier/contenu'); 3971 if (action === 'change_payment' && selected_payment_mode) { 3972 const queryParams = new URLSearchParams({tmp_payment_mode: selected_payment_mode}).toString(); 3973 cartUrl = cartUrl + '?' + queryParams; 3974 } 3975 fetch(cartUrl, { 3976 method: 'GET', 3977 headers: { 3978 'X-Requested-With': 'XMLHttpRequest', 3979 'Accept': 'text/html' 3980 }, 3981 credentials: 'same-origin' 3982 }) 3983 .then(response => { 3984 if (!response.ok || response.redirected) { 3985 window.location = cartPath; 3986 return null; 3987 } 3988 return response.text(); 3989 }) 3990 .then(body => { 3991 if (!body) {
3992 $("#cart-page > .cart, #cart-page > .cart-content").removeClass("loading"); 3993 return; 3994 } 3995 3996 $(cartContent).replaceWith(body); 3997 3998 checkShippingModeRelais(); 3999 displayMessages(response, display_error); 4000 4001 // Process new status messages after Drupal.Message renders them 4002 setTimeout(() => window.handleStatusMessages?.(), 100); 4003 4004 if (action === 'change_payment' && selected_payment_mode) { 4005 document.querySelector('input[name="paymentMode"][value="' + selected_payment_mode + '"]').checked = true; 4006 } 4007 4008 // Mise à jour de la quantité d'articles sur l'icône du header 4009 const cartMenuItem = document.querySelector('header a.cart_link'); 4010 const url = getUrl('panier/current/item_count'); 4011 fetch(url, { 4012 method: 'GET', 4013 headers: { 4014 'Accept': 'application/json', 4015 } 4016 }).then(response => response.json()).then(json => { 4017 if (json?.total_qte) { 4018 cartMenuItem.setAttribute('data-cart-qty', json.total_qte); 4019 const totalsItem = document.querySelector('.totalitems'); 4020 if (totalsItem) { 4021 totalsItem.textContent = '(' + json.total_qte + ')'; 4022 } 4023 if (qtyBadge = cartMenuItem.querySelector('.qty-badge')) { 4024 qtyBadge.innerHTML = json.total_qte; 4025 } 4026 } 4027 // Remove loading state after all operations complete 4028 $("#cart-page > .cart, #cart-page > .cart-content").removeClass("loading"); 4029 }); 4030 }) 4031 .catch(error => { 4032 console.error('Cart content fetch error:', error); 4033 $("#cart-page > .cart, #cart-page > .cart-content").removeClass("loading"); 4034 window.location = cartPath; 4035 }); 4036 } catch (error) { 4037 console.error('Cart refresh error:', error); 4038 $("#cart-page > .cart, #cart-page > .cart-content").removeClass("loading"); 4039 window.location = cartPath; 4040 } 4041 } else { 4042 // Fallback du vieux comportement si thème mal configuré : On recharge complètement la page 4043 window.location = parser.href; 4044 } 4045 } 4046 }); 4047} 4048 4049function displayMessages(response, display_error = false) { 4050 const messages = new Drupal.Message(); 4051 4052 // Affichage des messages de notification 4053 let msgs = []; 4054 if(response?.message) { 4055 msgs.push(response.message) 4056 } 4057 if(response?.messages) { 4058 response.messages.forEach(msg => { 4059 msgs.push(msg) 4060 }) 4061 } 4062 // Si c'est un objet (avec type et texte) 4063 msgs.forEach(msg => { 4064 display_error = false 4065 if(typeof msg == "object" && msg?.text) { 4066 messages.add( 4067 Drupal.t(msg.text), 4068 {type: msg.type || 'status'} 4069 ) 4070 } else { 4071 // Sinon c'est directement du texte 4072 messages.add( 4073 Drupal.t(msg) 4074 ) 4075 } 4076 }) 4077 4078 // On n'affiiche l'erreur que si aucune notification n'a été affichée 4079 if(display_error) { 4080 messages.add( 4081 Drupal.t("Une erreur est survenue"), 4082 {type: "error"} 4083 ) 4084 } 4085} 4086 4087function handleCartResponse(article_infos = null, addedId = null, addedQty = null, context = null) { 4088 // Formattage des articles lies 4089 let articles_lies = []; 4090 if (article_infos?.liaisons) { 4091 article_infos.liaisons.forEach(type_liaison => { 4092 if (type_liaison.sens_liaison == "AB") { 4093 type_liaison?.articles.forEach(art => { 4094 if (art.params.includes("EcomPopupPanierCrossSell")) { 4095 art.catalog_id = 1; 4096 if (art.catalog_ids?.length) { 4097 art.catalog_id = art.catalog_ids[0]; 4098 } 4099 articles_lies.push(art); 4100 } 4101 }); 4102 } 4103 }); 4104 } 4105 4106 fetch(getUrl("cart/content"), { 4107 method: 'GET', 4108 headers: { 4109 'Accept': 'application/json', 4110 } 4111 }) 4112 .then(response => response.json()) 4113 .then(json => { 4114 let cart = json.cart; 4115 let qty = 0; 4116 let qtyMessage = ''; 4117 4118 if (addedId) { 4119 cart.contenu.forEach(element => {
4120 if (element.id_article == addedId) { 4121 qty += parseInt(element.qte); 4122 if (element.qte == element.quota_min) { 4123 qtyMessage = " (quantité minimale de commande pour cet article)."; 4124 } else if (element.qte == element.quota_max) { 4125 qtyMessage = " (quantité maximale de commande pour cet article)."; 4126 } 4127 } 4128 }); 4129 } 4130 if (context) context.isLoading = false; 4131 Alpine.store('modal').open({ 4132 id: 'added_to_cart', 4133 qty: addedQty || qty, 4134 qtyMessage, 4135 cart, 4136 articles_lies 4137 }); 4138 4139 // Scroll sur l'article qui vient d'être ajouté 4140 if (addedId) { 4141 Alpine.nextTick(function () { 4142 let articleElm = document.querySelector(`.article-panier[data-product-id="${addedId}"]`); 4143 if (articleElm) { 4144 // Délai pour la transition 4145 setTimeout(function () { 4146 articleElm.scrollIntoView(); 4147 }, 1); 4148 } 4149 }) 4150 } 4151 4152 loadCartQty(cart); 4153 }); 4154} 4155 4156function cartStep(step) { 4157 $("#cart-page").attr("data-step", step); 4158 $(`.tab-title:not([data-step="${step}"])`).removeClass("is-active"); 4159 $(`.tab-title[data-step="${step}"]`).addClass("is-active"); 4160} 4161 4162function checkMaxLengthTypeNumber(input, max) 4163{ 4164 if (input.value.length > max) { 4165 input.value = input.value.slice(0, max); 4166 } 4167} 4168 4169function saveToLocalStorage(field, value) { 4170 const saved_data = localStorage.getItem('saved_input_floa'); 4171 let parsed_data = {}; 4172 if (saved_data) { 4173 parsed_data = JSON.parse(saved_data); 4174 } 4175 parsed_data[field] = value; 4176 localStorage.setItem('saved_input_floa', JSON.stringify(parsed_data)); 4177} 4178 4179function submitPaymentPage(redirect_url, redirection_data) 4180{ 4181 const form = $("<form>", { 4182 method: "POST", 4183 action: redirect_url, 4184}); 4185 4186 $("<input>", { 4187 type: "text", 4188 name: "redirectionData", 4189 id: "redirectionData", 4190 value: redirection_data, 4191 }).appendTo(form); 4192 4193 $("<input>", { 4194 type: "submit", 4195 value: "Submit", 4196 }).appendTo(form); 4197 4198 form.appendTo("body").submit(); 4199} 4200 4201/** 4202 * Ajout des events listeners 4203 */ 4204(function ($, Drupal) { 4205 // Modification de la quantité sur la page récapitulative 4206 $(document).off("change", ".cart-qty"); 4207 $(document).on("change", ".cart-qty", function () { 4208 updateCartContent(this, $(this).val()); 4209 }); 4210 4211 // Suppression d'une ligne sur la page récapitulative 4212 $(document).off("click", ".cart-remove"); 4213 $(document).on("click", ".cart-remove", function () { 4214 removeCartContent(this); 4215 }); 4216 // delete all lines 4217 $(document).on("click", "#clear-cart", function () { 4218 removeCartContent($(".cart-item")); 4219 }); 4220 4221 // Onglets 4222 $(document).off("click", "#cart-tabs .tab-title"); 4223 $(document).on("click", "#cart-tabs .tab-title", function () { 4224 cartStep($(this).attr("data-step")); 4225 }); 4226 4227 // Confirmation du panier (renvoie sur la page transport) 4228 $(document).off("click", ".cart-confirm"); 4229 $(document).on("click", ".cart-confirm", function () { 4230 cartStep("shipping"); 4231 }); 4232 4233 // Modification du mode de livraison sélectionné 4234 $(document).off("change", ".shipping-selector"); 4235 $(document).on("change", ".shipping-selector", function () { 4236 if ($(this).is(":checked")) { 4237 if ($(this).data("type-livraison") === "relais") { 4238 $(this).prop("checked", false); 4239 currentTransportModeElm = this; 4240 getPointRelais(this); 4241 } else if ($(this).data("type-livraison") === "enlevement") { 4242 localStorage.setItem('showListMagasin', JSON.stringify(true)); 4243 updateShippingMode(this); 4244 } else { 4245 updateShippingMode(this); 4246 } 4247 } 4248 }); 4249 4250 $(document).off("click", ".change-point-relais"); 4251 $(document).on("click", ".change-point-relais", function () { 4252 let selectedShippingMode = $(".shipping-selector:checked"); 4253 if (selectedShippingMode.length > 0) { 4254 currentTransportModeElm = selectedShippingMode;
4255 getPointRelais(selectedShippingMode, $(this).data("code_pr")); 4256 } 4257 }); 4258 4259 let shippingAvailable = $('input[name="shipping-mode"]'); 4260 let shippingSelected = $('input[name="shipping-mode"]:checked'); 4261 4262 $('#multi-livraison-mode').hide(); 4263 if(shippingAvailable.length > 0) { 4264 if (shippingSelected.length > 0) { 4265 $('#multi-livraison-mode').show(); 4266 } else { 4267 $('#multi-livraison-mode').hide(); 4268 } 4269 } 4270 4271 // Modification shipping mode 4272 $(document).off("click", "#ship_separately"); 4273 $(document).on("click", "#ship_separately", function (evt) { 4274 updateMultiLivaraisonMode(livraison_mode = 'ship_separately'); 4275 }); 4276 4277 $(document).off("click", "#wait_all_available"); 4278 $(document).on("click", "#wait_all_available", function (evt) { 4279 updateMultiLivaraisonMode(livraison_mode = 'wait_all_available'); 4280 }); 4281 4282 // Validation du panier 4283 $(document).off("click", ".cart-checkout"); 4284 $(document).on("click", ".cart-checkout", function (evt) { 4285 validateCart(evt); 4286 }); 4287 4288 // View detail article 4289 $(document).on("click", ".view-detail-article, .article-hover-link", function (evt) { 4290 let dataArticle = $(this).attr("data-article"); 4291 if(!dataArticle) { 4292 dataArticle = $(this).parent().attr("data-article"); 4293 } 4294 dataArticle = htmlDecode(dataArticle); 4295 4296 if(dataArticle) { 4297 try { 4298 dataArticle = JSON.parse(dataArticle) 4299 pushDataLayer(dataArticle, "select_item", "catalogue", "Catalogue client") 4300 } catch(err) { 4301 console.error(err) 4302 } 4303 } 4304 evt.stopPropagation(); 4305 }); 4306 4307 // Modification de l'adresse 4308 $(document).off("change", '[name="adresse-livraison"], [name="adresse-facturation"]'); 4309 $(document).on("change", '[name="adresse-livraison"], [name="adresse-facturation"]', function () { 4310 if ($(this).is(":checked")) { 4311 updateAddress(this); 4312 } 4313 }); 4314 4315 // Bouton "Nouvelle adresse" (livraison + facturation) â ouvre le formulaire de création 4316 $(document).off("click", "#btn-new-adresse-livraison, #btn-new-adresse-facturation"); 4317 $(document).on("click", "#btn-new-adresse-livraison, #btn-new-adresse-facturation", function () { 4318 const type_adresse = this.id === "btn-new-adresse-livraison" ? "livraison" : "facturation"; 4319 $(`#cart-add-adresse-${type_adresse}`).show(); 4320 }); 4321 4322 // Payment method 4323 $(document).on("click", ".payment-choice input", function (evt) { 4324 const selected_label_payment = $('label[for="' + this.id + '"]'); 4325 const payment_type = selected_label_payment.text().trim(); 4326 let data_article = $("#cart-page").attr("data-article"); 4327 let data_cart_info = $("#cart-page").attr("data-cart-info"); 4328 try { 4329 data_cart_info = JSON.parse(data_cart_info); 4330 let option_data = { 4331 'payment_type': payment_type, 4332 'currency': data_cart_info.abrev_devise, 4333 } 4334 data_article = JSON.parse(htmlDecode(data_article)); 4335 pushMutilDataLayer(data_article, "add_payment_info", "view_cart", "View cart", option_data); 4336 } catch(err) { 4337 console.error("Erreur lors du stockage des informations sur la transaction", err); 4338 } 4339 4340 if($(this).parent().find('.module-info').length) { 4341 let content = $(this).parent().find('.module-info').html(); 4342 4343 $('.chequee-modal').html(content); 4344 Alpine.store('modal').open({ 4345 id: 'cheque_modal' 4346 }); 4347 } 4348 4349 if($(this).parent().find('.module-mandat-info').length) { 4350 let content = $(this).parent().find('.module-mandat-info').html(); 4351 4352 $('.mandat-modal').html(content); 4353 Alpine.store('modal').open({ 4354 id: 'mandat_modal' 4355 }); 4356 } 4357 4358 if ($(this).data('module') === "floa") { 4359 const parsed_data_storage = JSON.parse(localStorage.getItem('saved_input_floa')); 4360 Alpine.store('modal').open({ 4361 id: 'floa_information_modal', 4362 authorized_country: $(this).data('authorized-country'), 4363 adresse_facturation_tel: $(this).data('adresse-facturation-tel'), 4364 civility: $(this).data('civility'), 4365 data_storage: parsed_data_storage 4366 }); 4367 } else { 4368 // clear data form 4369 localStorage.removeItem('saved_input_floa'); 4370 } 4371 4372 }); 4373 4374 // Shipping 4375 $(document).on("click", ".mode-livraison input", function (evt) {
4376 let data_article = $("#cart-page").attr("data-article"); 4377 data_article = JSON.parse(htmlDecode(data_article)); 4378 pushMutilDataLayer(data_article, "add_shipping_info", "view_cart", "View cart") 4379 }); 4380 // Floa information 4381 $(document).on("click", ".valider_floa_info_btn", function (evt) { 4382 // Set params 4383 const authorized_country = $(this).data('authorized-country'); 4384 switch(authorized_country) { 4385 case "ES": 4386 // Check box the term ES 4387 let term_es1 = $("#term_es1").is(":checked"); 4388 let term_es2 = $("#term_es2").is(":checked"); 4389 if (!term_es1 || !term_es2) { 4390 $("#floa-term-es-err").show(); 4391 window.location.href = "#term_es1"; 4392 return; 4393 } 4394 $("#floa-term-es-err").hide(); 4395 4396 var national_id = $(this).closest(".floa-information-modal").find(".national_id").val(); 4397 var date_of_birth = $(this).closest(".floa-information-modal").find(".date_of_birth").val(); 4398 var second_lastname = $(this).closest(".floa-information-modal").find(".second_lastname").val(); 4399 4400 if (!national_id || !date_of_birth) { 4401 $("#floa-term-es-err-empty").show(); 4402 return; 4403 } else { 4404 $("#floa-term-es-err-empty").hide(); 4405 const floa_params = { 4406 national_id: national_id, 4407 date_of_birth: date_of_birth, 4408 second_lastname: second_lastname ?? '', 4409 }; 4410 localStorage.setItem('floa_params', JSON.stringify(floa_params)); 4411 Alpine.store('modal').close(); 4412 } 4413 break; 4414 4415 case "FR": 4416 // Check box the term FR 4417 let term_fr1 = $("#term_fr1").is(":checked"); 4418 let term_fr2 = $("#term_fr2").is(":checked"); 4419 if (!term_fr1 || !term_fr2) { 4420 $("#floa-term-fr-err").show(); 4421 window.location.href = "#term_fr1"; 4422 return; 4423 } 4424 $("#floa-term-fr-err").hide(); 4425 4426 var maiden_name = $(this).closest(".floa-information-modal").find(".maiden_name").val(); 4427 var maiden_name_required = $(this).closest(".floa-information-modal").find(".maiden_name").data('maiden-name-is-required'); 4428 var code_postal_birth = $(this).closest(".floa-information-modal").find(".code_postal_birth").val(); 4429 var date_of_birth = $(this).closest(".floa-information-modal").find(".date_of_birth").val(); 4430 var phone_number = $(this).closest(".floa-information-modal").find(".phone_number").val(); 4431 var phone_number_required = $(this).closest(".floa-information-modal").find(".phone_number").data('phone-is-required'); 4432 4433 if ((!maiden_name && maiden_name_required) || !code_postal_birth || !date_of_birth ||( !phone_number && phone_number_required)) { 4434 $("#floa-term-fr-err-empty").show(); 4435 return; 4436 } else { 4437 $("#floa-term-fr-err-empty").hide(); 4438 const floa_params = { 4439 maiden_name: maiden_name, 4440 code_postal_birth: code_postal_birth, 4441 date_of_birth: date_of_birth, 4442 phone_number: phone_number 4443 }; 4444 localStorage.setItem('floa_params', JSON.stringify(floa_params)); 4445 Alpine.store('modal').close(); 4446 } 4447 break; 4448 } 4449 4450 // clear data form 4451 localStorage.removeItem('saved_input_floa'); 4452 }); 4453 4454 $(document).ready(function() { 4455 $(document).on("mouseenter mouseleave", ".magasins__pickup--option:first-of-type", function () { 4456 $(this).parents(".magasins__pickup--options").toggleClass("option-hover"); 4457 }); 4458 4459 $(document).on("click", ".magasins__pickup--select-trigger", function (event) { 4460 $(this).parents(".magasins__pickup--select").toggleClass("opened"); 4461 event.stopPropagation(); 4462 }); 4463 4464 $(document).on("click", ".magasins__pickup--option", function () { 4465 // const selected_value = $(this).data("value"); 4466 // $(this).parents(".magasins__time--select-box").find("select").val(selected_value); 4467 $(this).parents(".magasins__pickup--options").find(".magasins__pickup--option").removeClass("selection"); 4468 $(this).addClass("selection"); 4469 $(this).parents(".magasins__pickup--select").removeClass("opened").find(".magasins__pickup--select-trigger").text($(this).text()); 4470 }); 4471 }); 4472 4473 //Code promotion 4474 $(document).on("click", ".apply-coupon", function (evt) {
4475 let code_coupon = $('input[name="code_promotion"]').val(); 4476 if (code_coupon) { 4477 let id_panier = $("#cart-page").attr("data-cart"); 4478 if (!id_panier) { 4479 console.error("ID panier non trouvé"); 4480 return; 4481 } 4482 let data = { 4483 id_panier, 4484 code_coupon 4485 }; 4486 cartAjax("cart/coupon", "POST", data, "coupon"); 4487 } else { 4488 displayMessages( 4489 { 4490 message: { 4491 text: Drupal.t('Code promotion non renseigné'), 4492 type: 'warning' 4493 } 4494 } 4495 ) 4496 } 4497 }); 4498 4499 $(document).on("click", ".remove-coupon", function () { 4500 let code_coupon = $(this).data("code"); 4501 let id_am = $(this).data("id_am"); 4502 let id_panier = $("#cart-page").attr("data-cart"); 4503 if (!id_panier || !code_coupon) { 4504 console.error("ID panier ou code coupon manquant"); 4505 return; 4506 } 4507 cartAjax("cart/coupon/remove", "POST", { id_panier, code_coupon, id_am }, "coupon"); 4508 }); 4509 4510 $(document).ready(function () { 4511 checkShippingModeRelais(); 4512 }); 4513 4514 // Appliquer / Annuler un paiement par points fidélité via checkbox 4515 $(document).on("change", ".fidelite-toggle", function () { 4516 const isChecked = $(this).is(":checked");
4517 let id_panier = $("#cart-page").attr("data-cart"); 4518 let id_fid_program = $(".fidelite-input").data("id-fid-programme"); 4519 if (!id_panier) { 4520 console.error("ID panier non trouvé"); 4521 return; 4522 } 4523 let data = { id_panier, id_fid_program }; 4524 if (isChecked) { 4525 let nb_points = parseInt($(this).data("points")); 4526 if (!nb_points || nb_points <= 0) { 4527 return; 4528 } 4529 data.nb_points = nb_points; 4530 } 4531 4532 let path = isChecked ? "cart/fidelite/payer-points" : "cart/fidelite/annuler-paiement"; 4533 cartAjax(path, "POST", data, "fidelite"); 4534 }); 4535 4536 Drupal.behaviors.addressMaxLengthValidation = { 4537 attach: function (context, settings) { 4538 if (context !== document) { 4539 return; 4540 } 4541 if (window.addressMaxLengthValidationInitialized) { 4542 return; 4543 } 4544 window.addressMaxLengthValidationInitialized = true; 4545 4546 function showAddressMaxLengthMessage(maxLength) { 4547 const messageText = Drupal.t("L'adresse ne peut pas dépasser @max caractères.", {'@max': maxLength}); 4548 const messageWrapper = document.querySelector('[data-drupal-messages]'); 4549 if (messageWrapper && messageWrapper.textContent.includes(messageText)) { 4550 return; 4551 } 4552 4553 const messages = new Drupal.Message(); 4554 messages.add(messageText, { type: 'error' }); 4555 4556 if (window.handleStatusMessages) { 4557 window.handleStatusMessages(); 4558 } 4559 } 4560 4561 $(document).on('keydown', 'input[name="line1"], input[name="line2"], input[name$="[ligne_1]"], input[name$="[ligne_2]"]', function(e) { 4562 const $input = $(this); 4563 const maxLength = parseInt($input.attr('maxlength'), 10); 4564 if (!maxLength || maxLength <= 0) return; 4565 4566 if (e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey) { 4567 const start = this.selectionStart; 4568 const end = this.selectionEnd; 4569 const selectedLength = end - start; 4570 const currentLength = this.value.length; 4571 const nextLength = currentLength - selectedLength + 1; 4572 4573 if (nextLength > maxLength) { 4574 $input.addClass("border-error"); 4575 showAddressMaxLengthMessage(maxLength); 4576 e.preventDefault(); 4577 return false; 4578 } else { 4579 $input.removeClass("border-error"); 4580 } 4581 } 4582 }); 4583 4584 $(document).on('paste', 'input[name="line1"], input[name="line2"], input[name$="[ligne_1]"], input[name$="[ligne_2]"]', function(e) { 4585 const $input = $(this); 4586 const maxLength = parseInt($input.attr('maxlength'), 10); 4587 if (!maxLength || maxLength <= 0) return; 4588 4589 const clipboardData = e.originalEvent.clipboardData || window.clipboardData; 4590 if (clipboardData) { 4591 const pastedText = clipboardData.getData('text') || ''; 4592 const start = this.selectionStart; 4593 const end = this.selectionEnd; 4594 const selectedLength = end - start; 4595 const currentLength = this.value.length; 4596 const nextLength = currentLength - selectedLength + pastedText.length; 4597 4598 if (nextLength > maxLength) { 4599 $input.addClass("border-error"); 4600 showAddressMaxLengthMessage(maxLength); 4601 e.preventDefault(); 4602 return false; 4603 } else { 4604 $input.removeClass("border-error"); 4605 } 4606 } 4607 }); 4608 4609 $(document).on('input', 'input[name="line1"], input[name="line2"], input[name$="[ligne_1]"], input[name$="[ligne_2]"]', function() { 4610 const $input = $(this); 4611 const maxLength = parseInt($input.attr('maxlength'), 10); 4612 if (!maxLength || maxLength <= 0) return; 4613 4614 if (this.value.length < maxLength) { 4615 $input.removeClass("border-error"); 4616 } 4617 }); 4618 } 4619 }; 4620 4621})(jQuery, Drupal); 4622 4623function openDeclinaisonSelectionModal(carac) { 4624 let noOneHasMarqueur = carac.values.every(item => !item.marqueur); 4625 carac.noOneHasMarqueur = noOneHasMarqueur 4626 Alpine.store('modal').open({ 4627 id: 'declinaison-selection', 4628 carac: carac, 4629 }); 4630} 4631 4632function openOptionPersoSelectionModal(option) { 4633 let noOneHasMarqueur = !option.marqueurs || Object.values(option.marqueurs).every(m => !m); 4634 option.noOneHasMarqueur = noOneHasMarqueur; 4635 Alpine.store('modal').open({ 4636 id: 'optionPerso-selection', 4637 option: option, 4638 }); 4639} 4640 4641function toggleAdresseLivraisonSection(show) { 4642 const $elements = $('#titre-cart-adresse-livraison, #break-cart-adresse-livraison, #cart-adresses-livraison'); 4643 show ? $elements.show() : $elements.hide(); 4644} 4645 4646function checkShippingModeRelais() { 4647 const typeTransport = $('.shipping-selector:checked').data('type-livraison'); 4648 toggleAdresseLivraisonSection(typeTransport !== 'relais'); 4649}
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.