templates/frontView/product/_form.html.twig line 1

Open in your IDE?
  1. {# Section Coupon #}
  2. <div class="mb-3">
  3.     <label class="form-label text-white fw-bold">
  4.         <i class="bi bi-tag-fill me-1"></i> Code coupon
  5.     </label>
  6.     <div class="input-group">
  7.         <input 
  8.             type="text" 
  9.             id="coupon_code" 
  10.             class="form-control text-uppercase" 
  11.             placeholder="Ex: CPON255"
  12.             maxlength="50"
  13.         >
  14.         <button class="btn btn-warning fw-bold" type="button" id="btn_apply_coupon">
  15.             Appliquer
  16.         </button>
  17.     </div>
  18.     <div id="coupon_msg" class="mt-1 small"></div>
  19. </div>
  20. {# Ligne de réduction (Badge supprimé) #}
  21. <div class="justify-content-between" id="ligne_reduction" style="display:none">
  22.     <p class="mb-2 text-white">Réduction</p>
  23.     <p class="mb-2 text-warning fw-bold">
  24.         - <span id="montant_reduction">0.00</span>
  25.     </p>
  26. </div>
  27. {# Bouton pour retirer le coupon #}
  28. <div id="btn_remove_coupon_wrapper" style="display:none">
  29.     <button type="button" class="btn btn-sm btn-outline-warning w-100 mb-2" id="btn_remove_coupon">
  30.         <i class="bi bi-x-circle me-1"></i> Retirer le coupon
  31.     </button>
  32. </div>
  33. <hr class="text-white">
  34. {# Total Final après remise #}
  35. <div class="justify-content-between fw-bold" id="ligne_total_final" style="display:none">
  36.     <p class="mb-2 text-white">Total après réduction</p>
  37.     <p class="mb-2 text-warning" id="total_apres_reduction">0.00</p>
  38. </div>
  39. {#
  40.     La création de commande ne se fait plus depuis ce formulaire.
  41.     Le paiement sera connecté au serveur dans une prochaine étape.
  42. #}
  43. {% if listPanier is not defined or listPanier|length > 0 %}
  44.     <div class="cart-checkout-action mt-3">
  45.         {% if is_granted('ROLE_CUSTOMER') %}
  46.             <button
  47.                 type="button"
  48.                 class="btn btn-cart-checkout w-100"
  49.                 data-cart-modal-target="#mobileMoneyModal"
  50.             >
  51.                 <span>Valider ma commande</span>
  52.                 <i class="fas fa-arrow-right"></i>
  53.             </button>
  54.             <p class="cart-checkout-hint">
  55.                 <i class="fas fa-lock me-1"></i>
  56.                 Paiement sécurisé par Mobile Money
  57.             </p>
  58.         {% else %}
  59.             <button
  60.                 type="button"
  61.                 class="btn btn-cart-checkout w-100"
  62.                 data-cart-modal-target="#customerLoginRequiredModal"
  63.             >
  64.                 <span>Valider ma commande</span>
  65.                 <i class="fas fa-arrow-right"></i>
  66.             </button>
  67.             <p class="cart-checkout-hint">
  68.                 <i class="fas fa-user-lock me-1"></i>
  69.                 Connectez-vous pour continuer le paiement
  70.             </p>
  71.         {% endif %}
  72.     </div>
  73. {% endif %}
  74. <span id="csrf_coupon_remove" data-token="{{ csrf_token('coupon_remove') }}" style="display:none"></span>
  75. <script>
  76. document.addEventListener('DOMContentLoaded', function () {
  77.     const btnApply       = document.getElementById('btn_apply_coupon');
  78.     const btnRemove      = document.getElementById('btn_remove_coupon');
  79.     const couponInput    = document.getElementById('coupon_code');
  80.     const couponMsg      = document.getElementById('coupon_msg');
  81.     const ligneReduction = document.getElementById('ligne_reduction');
  82.     const ligneTotal     = document.getElementById('ligne_total_final');
  83.     const btnRemoveWrap  = document.getElementById('btn_remove_coupon_wrapper');
  84.     const csrfToken      = document.getElementById('csrf_coupon_remove').dataset.token;
  85.     // ── Fonction de formatage comme number_format(2, '.', ' ') ──
  86.     function formatPrice(amount) {
  87.         if (isNaN(amount)) amount = 0;
  88.         return parseFloat(amount).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
  89.     }
  90.     // ── Récupérer le total TTC depuis le DOM ──
  91.     function getTotalTtc() {
  92.         const el = document.getElementById('totalCommandeTtc');
  93.         if (!el) return 0;
  94.         // Nettoyage des espaces et conversion en float
  95.         const raw = el.innerText.replace(/\s/g, '').replace(',', '.');
  96.         return parseFloat(raw) || 0;
  97.     }
  98.     function setMobileMoneyAmount(amount) {
  99.         const mobileMoneyAmount = document.getElementById('mobileMoneyAmount');
  100.         if (mobileMoneyAmount) {
  101.             mobileMoneyAmount.textContent = formatPrice(parseFloat(amount) || 0);
  102.         }
  103.     }
  104.     // ── Affichage des messages de succès / erreur ──
  105.     function showMsg(text, type = 'success') {
  106.         couponMsg.innerHTML = `<span class="text-${type === 'success' ? 'success' : 'danger'}">${text}</span>`;
  107.     }
  108.     // ── Réinitialiser le bouton appliquer ──
  109.     function resetBtn() {
  110.         btnApply.disabled    = false;
  111.         btnApply.textContent = 'Appliquer';
  112.     }
  113.     // ── Appliquer le coupon ──
  114.     btnApply.addEventListener('click', function () {
  115.         const code  = couponInput.value.trim();
  116.         const total = getTotalTtc();
  117.         if (!code) {
  118.             showMsg('Veuillez saisir un code coupon.', 'error');
  119.             return;
  120.         }
  121.         if (total <= 0) {
  122.             showMsg('Votre panier est vide.', 'error');
  123.             return;
  124.         }
  125.         btnApply.disabled    = true;
  126.         btnApply.textContent = '...';
  127.         fetch('/coupon/apply', {
  128.             method: 'POST',
  129.             headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  130.             body: 'coupon_code=' + encodeURIComponent(code) + '&order_total=' + encodeURIComponent(total)
  131.         })
  132.         .then(r => r.json())
  133.         .then(data => {
  134.             if (data.success) {
  135.                 showMsg(data.message, 'success');
  136.                 // Mise à jour des chiffres avec formatage
  137.                 document.getElementById('montant_reduction').textContent   = formatPrice(parseFloat(data.discountAmount));
  138.                 document.getElementById('total_apres_reduction').textContent = formatPrice(parseFloat(data.totalAfterDiscount));
  139.                 setMobileMoneyAmount(data.totalAfterDiscount);
  140.                 // Affichage des lignes cachées
  141.                 ligneReduction.style.display = 'flex';
  142.                 ligneTotal.style.display     = 'flex';
  143.                 btnRemoveWrap.style.display  = 'block';
  144.                 
  145.                 couponInput.readOnly         = true;
  146.                 btnApply.style.display       = 'none';
  147.             } else {
  148.                 showMsg(data.message, 'error');
  149.                 resetBtn();
  150.             }
  151.         })
  152.         .catch(err => {
  153.             showMsg('Erreur réseau.', 'error');
  154.             resetBtn();
  155.         });
  156.     });
  157.     // ── Retirer le coupon ──
  158.     btnRemove.addEventListener('click', function () {
  159.         fetch('/coupon/remove', {
  160.             method: 'POST',
  161.             headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  162.             body: '_token=' + encodeURIComponent(csrfToken)
  163.         })
  164.         .then(r => r.json())
  165.         .then(data => {
  166.             if (data.success) {
  167.                 couponInput.value       = '';
  168.                 couponInput.readOnly    = false;
  169.                 couponMsg.innerHTML     = '';
  170.                 ligneReduction.style.display = 'none';
  171.                 ligneTotal.style.display     = 'none';
  172.                 btnRemoveWrap.style.display  = 'none';
  173.                 btnApply.style.display       = 'block';
  174.                 setMobileMoneyAmount(getTotalTtc());
  175.                 resetBtn();
  176.             }
  177.         });
  178.     });
  179.     // ── Entrée clavier pour appliquer le coupon ──
  180.     couponInput.addEventListener('keydown', function (e) {
  181.         if (e.key === 'Enter') {
  182.             e.preventDefault();
  183.             btnApply.click();
  184.         }
  185.     });
  186.     // ── Optionnel : formater le total initial au chargement ──
  187.     const elTotal = document.getElementById('totalCommandeTtc');
  188.     if (elTotal) {
  189.         const totalValue = parseFloat(elTotal.innerText.replace(/\s/g, '').replace(',', '.')) || 0;
  190.         elTotal.innerText = formatPrice(totalValue);
  191.     }
  192. });
  193. </script>
  194. {# Message affiché avant le paiement si le client n'est pas connecté #}
  195. {% if not is_granted('ROLE_CUSTOMER') %}
  196.     <div class="modal fade cart-modern-modal" id="customerLoginRequiredModal" tabindex="-1" aria-labelledby="customerLoginRequiredTitle" aria-hidden="true">
  197.         <div class="modal-dialog modal-dialog-centered modal-sm">
  198.             <div class="modal-content">
  199.                 <div class="modal-body text-center p-4">
  200.                     <button type="button" class="btn-close cart-login-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
  201.                     <div class="cart-modal-icon cart-modal-icon-primary">
  202.                         <i class="fas fa-user-lock"></i>
  203.                     </div>
  204.                     <h5 id="customerLoginRequiredTitle" class="cart-modal-title">Connexion nécessaire</h5>
  205.                     <p class="cart-modal-text">Connectez-vous à votre compte pour accéder au paiement de votre commande.</p>
  206.                     <div class="d-grid gap-2">
  207.                         <a href="/login" class="btn btn-cart-modal-primary">Se connecter</a>
  208.                         <a href="{{ path('app_home_inscription') }}" class="btn btn-cart-modal-light">Créer un compte</a>
  209.                     </div>
  210.                 </div>
  211.             </div>
  212.         </div>
  213.     </div>
  214. {% else %}
  215.     {# Interface Mobile Money : les appels opérateurs seront ajoutés ensuite. #}
  216.     <div class="modal fade cart-modern-modal" id="mobileMoneyModal" tabindex="-1" aria-labelledby="mobileMoneyTitle" aria-hidden="true">
  217.         <div class="modal-dialog modal-dialog-centered modal-md">
  218.             <div class="modal-content">
  219.                 <div class="modal-header cart-modal-header">
  220.                     <div>
  221.                         <span class="cart-modal-eyebrow"><i class="fas fa-shield-alt me-1"></i>Paiement sécurisé</span>
  222.                         <h5 id="mobileMoneyTitle" class="modal-title">Payer par Mobile Money</h5>
  223.                     </div>
  224.                     <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Fermer"></button>
  225.                 </div>
  226.                 <div class="modal-body p-4">
  227.                     <div class="cart-payment-total">
  228.                         <span>Total à payer</span>
  229.                         <strong><span id="mobileMoneyAmount">{{ total_taxe|number_format(2, '.', ' ') }}</span></strong>
  230.                     </div>
  231.                     <div id="mvolaPaymentFormFields">
  232.                     <p class="cart-payment-label">Choisissez votre opérateur</p>
  233.                     <div class="cart-payment-operators" role="group" aria-label="Opérateur Mobile Money">
  234.                         <button type="button" class="cart-operator-card is-selected" data-operator="mvola" aria-pressed="true">
  235.                             <img class="cart-operator-logo" src="{{ asset('img/mvola.svg') }}" alt="Logo MVola">
  236.                             <span>MVola</span>
  237.                             <i class="fas fa-check-circle"></i>
  238.                         </button>
  239.                         <button type="button" class="cart-operator-card" data-operator="orange" aria-pressed="false">
  240.                             <img class="cart-operator-logo" src="{{ asset('img/orange-money.svg') }}" alt="Logo Orange Money">
  241.                             <span>Orange Money</span>
  242.                             <i class="fas fa-check-circle"></i>
  243.                         </button>
  244.                     </div>
  245.                     <label for="mobileMoneyPhone" class="cart-payment-label">Numéro de téléphone</label>
  246.                     <div class="cart-phone-input">
  247.                         <span>+261</span>
  248.                         <input id="mobileMoneyPhone" type="tel" inputmode="numeric" maxlength="10" placeholder="34 00 000 00" autocomplete="tel">
  249.                     </div>
  250.                     <div class="cart-payment-info">
  251.                         <i class="fas fa-info-circle"></i>
  252.                         Vous recevrez une demande de confirmation sur votre téléphone.
  253.                     </div>
  254.                     </div>
  255.                     <div class="alert alert-warning d-none mt-3 mb-0" id="mobileMoneyInterfaceNotice" role="alert">
  256.                     </div>
  257.                 </div>
  258.                 <div class="modal-footer border-0 px-4 pb-4 pt-0">
  259.                     <button type="button" class="btn btn-cart-modal-light" data-bs-dismiss="modal">Annuler</button>
  260.                     <button
  261.                         type="button"
  262.                         class="btn btn-cart-modal-primary flex-grow-1"
  263.                         id="mobileMoneyPayButton"
  264.                         data-initiate-url="{{ path('app_mvola_initiate') }}"
  265.                         data-orange-initiate-url="{{ path('app_orange_initiate') }}"
  266.                         data-csrf-token="{{ csrf_token('mvola_payment') }}"
  267.                         data-orange-csrf-token="{{ csrf_token('orange_payment') }}"
  268.                     >
  269.                         Payer <i class="fas fa-arrow-right ms-2"></i>
  270.                     </button>
  271.                 </div>
  272.             </div>
  273.         </div>
  274.     </div>
  275. {% endif %}
  276. <script>
  277. document.addEventListener('DOMContentLoaded', function () {
  278.     // Bootstrap recommande que les modales soient des enfants directs de body.
  279.     // Le formulaire est inclus dans la carte du panier, qui crée un contexte
  280.     // d'empilement et pouvait placer le backdrop au-dessus de la modale.
  281.     document.querySelectorAll('.cart-modern-modal').forEach(function (modal) {
  282.         document.body.appendChild(modal);
  283.     });
  284.     document.querySelectorAll('.cart-modern-modal').forEach(function (modal) {
  285.         modal.addEventListener('shown.bs.modal', function () {
  286.             document.querySelectorAll('.modal-backdrop').forEach(function (backdrop) {
  287.                 backdrop.style.zIndex = '1990';
  288.             });
  289.         });
  290.     });
  291.     document.querySelectorAll('[data-cart-modal-target]').forEach(function (button) {
  292.         button.addEventListener('click', function () {
  293.             const target = document.querySelector(button.dataset.cartModalTarget);
  294.             if (!target || !window.bootstrap || !window.bootstrap.Modal) {
  295.                 return;
  296.             }
  297.             window.bootstrap.Modal.getOrCreateInstance(target).show();
  298.         });
  299.     });
  300.     const operatorButtons = document.querySelectorAll('.cart-operator-card');
  301.     const payButton = document.getElementById('mobileMoneyPayButton');
  302.     const phoneInput = document.getElementById('mobileMoneyPhone');
  303.     const notice = document.getElementById('mobileMoneyInterfaceNotice');
  304.     operatorButtons.forEach(function (button) {
  305.         button.addEventListener('click', function () {
  306.             operatorButtons.forEach(function (item) {
  307.                 item.classList.remove('is-selected');
  308.                 item.setAttribute('aria-pressed', 'false');
  309.             });
  310.             button.classList.add('is-selected');
  311.             button.setAttribute('aria-pressed', 'true');
  312.         });
  313.     });
  314.     if (payButton) {
  315.         payButton.addEventListener('click', function () {
  316.             if (!phoneInput.value.trim()) {
  317.                 phoneInput.focus();
  318.                 phoneInput.classList.add('is-invalid');
  319.                 return;
  320.             }
  321.             phoneInput.classList.remove('is-invalid');
  322.             const selectedOperator = document.querySelector('.cart-operator-card.is-selected');
  323.             const operator = selectedOperator ? selectedOperator.dataset.operator : 'mvola';
  324.             const amountElement = document.getElementById('mobileMoneyAmount');
  325.             const amount = amountElement ? amountElement.textContent.replace(/\s/g, '').replace(',', '.') : '0';
  326.             const isOrange = operator === 'orange';
  327.             const initiateUrl = isOrange ? payButton.dataset.orangeInitiateUrl : payButton.dataset.initiateUrl;
  328.             const csrfToken = isOrange ? payButton.dataset.orangeCsrfToken : payButton.dataset.csrfToken;
  329.             payButton.disabled = true;
  330.             payButton.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Envoi de la demande...';
  331.             notice.className = 'alert alert-info mt-3 mb-0';
  332.             notice.textContent = isOrange ? 'Connexion à Orange Money en cours...' : 'Connexion à MVola en cours...';
  333.             fetch(initiateUrl, {
  334.                 method: 'POST',
  335.                 headers: {
  336.                     'Content-Type': 'application/json',
  337.                     'Accept': 'application/json',
  338.                     'X-CSRF-TOKEN': csrfToken
  339.                 },
  340.                 body: JSON.stringify({
  341.                     amount: amount,
  342.                     customer_msisdn: phoneInput.value
  343.                 })
  344.             })
  345.                 .then(response => response.json().then(data => ({ response, data })))
  346.                 .then(({ response, data }) => {
  347.                     if (!response.ok || !data.success) {
  348.                         throw new Error(data.error || 'Impossible d’initier le paiement MVola.');
  349.                     }
  350.                     window.location.href = isOrange ? data.payment_url : data.redirect_url;
  351.                 })
  352.                 .catch(error => {
  353.                     payButton.disabled = false;
  354.                     payButton.innerHTML = 'Payer <i class="fas fa-arrow-right ms-2"></i>';
  355.                     notice.className = 'alert alert-danger mt-3 mb-0';
  356.                     notice.textContent = error.message;
  357.                 });
  358.         });
  359.     }
  360. });
  361. </script>