{# Section Coupon #}
<div class="mb-3">
<label class="form-label text-white fw-bold">
<i class="bi bi-tag-fill me-1"></i> Code coupon
</label>
<div class="input-group">
<input
type="text"
id="coupon_code"
class="form-control text-uppercase"
placeholder="Ex: CPON255"
maxlength="50"
>
<button class="btn btn-warning fw-bold" type="button" id="btn_apply_coupon">
Appliquer
</button>
</div>
<div id="coupon_msg" class="mt-1 small"></div>
</div>
{# Ligne de réduction (Badge supprimé) #}
<div class="justify-content-between" id="ligne_reduction" style="display:none">
<p class="mb-2 text-white">Réduction</p>
<p class="mb-2 text-warning fw-bold">
- <span id="montant_reduction">0.00</span>
</p>
</div>
{# Bouton pour retirer le coupon #}
<div id="btn_remove_coupon_wrapper" style="display:none">
<button type="button" class="btn btn-sm btn-outline-warning w-100 mb-2" id="btn_remove_coupon">
<i class="bi bi-x-circle me-1"></i> Retirer le coupon
</button>
</div>
<hr class="text-white">
{# Total Final après remise #}
<div class="justify-content-between fw-bold" id="ligne_total_final" style="display:none">
<p class="mb-2 text-white">Total après réduction</p>
<p class="mb-2 text-warning" id="total_apres_reduction">0.00</p>
</div>
{#
La création de commande ne se fait plus depuis ce formulaire.
Le paiement sera connecté au serveur dans une prochaine étape.
#}
{% if listPanier is not defined or listPanier|length > 0 %}
<div class="cart-checkout-action mt-3">
{% if is_granted('ROLE_CUSTOMER') %}
<button
type="button"
class="btn btn-cart-checkout w-100"
data-cart-modal-target="#mobileMoneyModal"
>
<span>Valider ma commande</span>
<i class="fas fa-arrow-right"></i>
</button>
<p class="cart-checkout-hint">
<i class="fas fa-lock me-1"></i>
Paiement sécurisé par Mobile Money
</p>
{% else %}
<button
type="button"
class="btn btn-cart-checkout w-100"
data-cart-modal-target="#customerLoginRequiredModal"
>
<span>Valider ma commande</span>
<i class="fas fa-arrow-right"></i>
</button>
<p class="cart-checkout-hint">
<i class="fas fa-user-lock me-1"></i>
Connectez-vous pour continuer le paiement
</p>
{% endif %}
</div>
{% endif %}
<span id="csrf_coupon_remove" data-token="{{ csrf_token('coupon_remove') }}" style="display:none"></span>
<script>
document.addEventListener('DOMContentLoaded', function () {
const btnApply = document.getElementById('btn_apply_coupon');
const btnRemove = document.getElementById('btn_remove_coupon');
const couponInput = document.getElementById('coupon_code');
const couponMsg = document.getElementById('coupon_msg');
const ligneReduction = document.getElementById('ligne_reduction');
const ligneTotal = document.getElementById('ligne_total_final');
const btnRemoveWrap = document.getElementById('btn_remove_coupon_wrapper');
const csrfToken = document.getElementById('csrf_coupon_remove').dataset.token;
// ── Fonction de formatage comme number_format(2, '.', ' ') ──
function formatPrice(amount) {
if (isNaN(amount)) amount = 0;
return parseFloat(amount).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
}
// ── Récupérer le total TTC depuis le DOM ──
function getTotalTtc() {
const el = document.getElementById('totalCommandeTtc');
if (!el) return 0;
// Nettoyage des espaces et conversion en float
const raw = el.innerText.replace(/\s/g, '').replace(',', '.');
return parseFloat(raw) || 0;
}
function setMobileMoneyAmount(amount) {
const mobileMoneyAmount = document.getElementById('mobileMoneyAmount');
if (mobileMoneyAmount) {
mobileMoneyAmount.textContent = formatPrice(parseFloat(amount) || 0);
}
}
// ── Affichage des messages de succès / erreur ──
function showMsg(text, type = 'success') {
couponMsg.innerHTML = `<span class="text-${type === 'success' ? 'success' : 'danger'}">${text}</span>`;
}
// ── Réinitialiser le bouton appliquer ──
function resetBtn() {
btnApply.disabled = false;
btnApply.textContent = 'Appliquer';
}
// ── Appliquer le coupon ──
btnApply.addEventListener('click', function () {
const code = couponInput.value.trim();
const total = getTotalTtc();
if (!code) {
showMsg('Veuillez saisir un code coupon.', 'error');
return;
}
if (total <= 0) {
showMsg('Votre panier est vide.', 'error');
return;
}
btnApply.disabled = true;
btnApply.textContent = '...';
fetch('/coupon/apply', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'coupon_code=' + encodeURIComponent(code) + '&order_total=' + encodeURIComponent(total)
})
.then(r => r.json())
.then(data => {
if (data.success) {
showMsg(data.message, 'success');
// Mise à jour des chiffres avec formatage
document.getElementById('montant_reduction').textContent = formatPrice(parseFloat(data.discountAmount));
document.getElementById('total_apres_reduction').textContent = formatPrice(parseFloat(data.totalAfterDiscount));
setMobileMoneyAmount(data.totalAfterDiscount);
// Affichage des lignes cachées
ligneReduction.style.display = 'flex';
ligneTotal.style.display = 'flex';
btnRemoveWrap.style.display = 'block';
couponInput.readOnly = true;
btnApply.style.display = 'none';
} else {
showMsg(data.message, 'error');
resetBtn();
}
})
.catch(err => {
showMsg('Erreur réseau.', 'error');
resetBtn();
});
});
// ── Retirer le coupon ──
btnRemove.addEventListener('click', function () {
fetch('/coupon/remove', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: '_token=' + encodeURIComponent(csrfToken)
})
.then(r => r.json())
.then(data => {
if (data.success) {
couponInput.value = '';
couponInput.readOnly = false;
couponMsg.innerHTML = '';
ligneReduction.style.display = 'none';
ligneTotal.style.display = 'none';
btnRemoveWrap.style.display = 'none';
btnApply.style.display = 'block';
setMobileMoneyAmount(getTotalTtc());
resetBtn();
}
});
});
// ── Entrée clavier pour appliquer le coupon ──
couponInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
btnApply.click();
}
});
// ── Optionnel : formater le total initial au chargement ──
const elTotal = document.getElementById('totalCommandeTtc');
if (elTotal) {
const totalValue = parseFloat(elTotal.innerText.replace(/\s/g, '').replace(',', '.')) || 0;
elTotal.innerText = formatPrice(totalValue);
}
});
</script>
{# Message affiché avant le paiement si le client n'est pas connecté #}
{% if not is_granted('ROLE_CUSTOMER') %}
<div class="modal fade cart-modern-modal" id="customerLoginRequiredModal" tabindex="-1" aria-labelledby="customerLoginRequiredTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-body text-center p-4">
<button type="button" class="btn-close cart-login-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
<div class="cart-modal-icon cart-modal-icon-primary">
<i class="fas fa-user-lock"></i>
</div>
<h5 id="customerLoginRequiredTitle" class="cart-modal-title">Connexion nécessaire</h5>
<p class="cart-modal-text">Connectez-vous à votre compte pour accéder au paiement de votre commande.</p>
<div class="d-grid gap-2">
<a href="/login" class="btn btn-cart-modal-primary">Se connecter</a>
<a href="{{ path('app_home_inscription') }}" class="btn btn-cart-modal-light">Créer un compte</a>
</div>
</div>
</div>
</div>
</div>
{% else %}
{# Interface Mobile Money : les appels opérateurs seront ajoutés ensuite. #}
<div class="modal fade cart-modern-modal" id="mobileMoneyModal" tabindex="-1" aria-labelledby="mobileMoneyTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-md">
<div class="modal-content">
<div class="modal-header cart-modal-header">
<div>
<span class="cart-modal-eyebrow"><i class="fas fa-shield-alt me-1"></i>Paiement sécurisé</span>
<h5 id="mobileMoneyTitle" class="modal-title">Payer par Mobile Money</h5>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body p-4">
<div class="cart-payment-total">
<span>Total à payer</span>
<strong><span id="mobileMoneyAmount">{{ total_taxe|number_format(2, '.', ' ') }}</span></strong>
</div>
<div id="mvolaPaymentFormFields">
<p class="cart-payment-label">Choisissez votre opérateur</p>
<div class="cart-payment-operators" role="group" aria-label="Opérateur Mobile Money">
<button type="button" class="cart-operator-card is-selected" data-operator="mvola" aria-pressed="true">
<img class="cart-operator-logo" src="{{ asset('img/mvola.svg') }}" alt="Logo MVola">
<span>MVola</span>
<i class="fas fa-check-circle"></i>
</button>
<button type="button" class="cart-operator-card" data-operator="orange" aria-pressed="false">
<img class="cart-operator-logo" src="{{ asset('img/orange-money.svg') }}" alt="Logo Orange Money">
<span>Orange Money</span>
<i class="fas fa-check-circle"></i>
</button>
</div>
<label for="mobileMoneyPhone" class="cart-payment-label">Numéro de téléphone</label>
<div class="cart-phone-input">
<span>+261</span>
<input id="mobileMoneyPhone" type="tel" inputmode="numeric" maxlength="10" placeholder="34 00 000 00" autocomplete="tel">
</div>
<div class="cart-payment-info">
<i class="fas fa-info-circle"></i>
Vous recevrez une demande de confirmation sur votre téléphone.
</div>
</div>
<div class="alert alert-warning d-none mt-3 mb-0" id="mobileMoneyInterfaceNotice" role="alert">
</div>
</div>
<div class="modal-footer border-0 px-4 pb-4 pt-0">
<button type="button" class="btn btn-cart-modal-light" data-bs-dismiss="modal">Annuler</button>
<button
type="button"
class="btn btn-cart-modal-primary flex-grow-1"
id="mobileMoneyPayButton"
data-initiate-url="{{ path('app_mvola_initiate') }}"
data-orange-initiate-url="{{ path('app_orange_initiate') }}"
data-csrf-token="{{ csrf_token('mvola_payment') }}"
data-orange-csrf-token="{{ csrf_token('orange_payment') }}"
>
Payer <i class="fas fa-arrow-right ms-2"></i>
</button>
</div>
</div>
</div>
</div>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Bootstrap recommande que les modales soient des enfants directs de body.
// Le formulaire est inclus dans la carte du panier, qui crée un contexte
// d'empilement et pouvait placer le backdrop au-dessus de la modale.
document.querySelectorAll('.cart-modern-modal').forEach(function (modal) {
document.body.appendChild(modal);
});
document.querySelectorAll('.cart-modern-modal').forEach(function (modal) {
modal.addEventListener('shown.bs.modal', function () {
document.querySelectorAll('.modal-backdrop').forEach(function (backdrop) {
backdrop.style.zIndex = '1990';
});
});
});
document.querySelectorAll('[data-cart-modal-target]').forEach(function (button) {
button.addEventListener('click', function () {
const target = document.querySelector(button.dataset.cartModalTarget);
if (!target || !window.bootstrap || !window.bootstrap.Modal) {
return;
}
window.bootstrap.Modal.getOrCreateInstance(target).show();
});
});
const operatorButtons = document.querySelectorAll('.cart-operator-card');
const payButton = document.getElementById('mobileMoneyPayButton');
const phoneInput = document.getElementById('mobileMoneyPhone');
const notice = document.getElementById('mobileMoneyInterfaceNotice');
operatorButtons.forEach(function (button) {
button.addEventListener('click', function () {
operatorButtons.forEach(function (item) {
item.classList.remove('is-selected');
item.setAttribute('aria-pressed', 'false');
});
button.classList.add('is-selected');
button.setAttribute('aria-pressed', 'true');
});
});
if (payButton) {
payButton.addEventListener('click', function () {
if (!phoneInput.value.trim()) {
phoneInput.focus();
phoneInput.classList.add('is-invalid');
return;
}
phoneInput.classList.remove('is-invalid');
const selectedOperator = document.querySelector('.cart-operator-card.is-selected');
const operator = selectedOperator ? selectedOperator.dataset.operator : 'mvola';
const amountElement = document.getElementById('mobileMoneyAmount');
const amount = amountElement ? amountElement.textContent.replace(/\s/g, '').replace(',', '.') : '0';
const isOrange = operator === 'orange';
const initiateUrl = isOrange ? payButton.dataset.orangeInitiateUrl : payButton.dataset.initiateUrl;
const csrfToken = isOrange ? payButton.dataset.orangeCsrfToken : payButton.dataset.csrfToken;
payButton.disabled = true;
payButton.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Envoi de la demande...';
notice.className = 'alert alert-info mt-3 mb-0';
notice.textContent = isOrange ? 'Connexion à Orange Money en cours...' : 'Connexion à MVola en cours...';
fetch(initiateUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
amount: amount,
customer_msisdn: phoneInput.value
})
})
.then(response => response.json().then(data => ({ response, data })))
.then(({ response, data }) => {
if (!response.ok || !data.success) {
throw new Error(data.error || 'Impossible d’initier le paiement MVola.');
}
window.location.href = isOrange ? data.payment_url : data.redirect_url;
})
.catch(error => {
payButton.disabled = false;
payButton.innerHTML = 'Payer <i class="fas fa-arrow-right ms-2"></i>';
notice.className = 'alert alert-danger mt-3 mb-0';
notice.textContent = error.message;
});
});
}
});
</script>