PageSourceSearch

https://laine-squirrelsyarns.com/modules/lundimatin-blocks/assets/js/cart.js?v=b3737abb

js laine-squirrelsyarns.com collected 2026-10-07 14:50:11 UTC 164,037 bytes, 4,649 lines download raw bytes

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.