feat(front): cart page with quantity controls and TVA breakdown
Displays line items with - / + controls and delete button. TVA 10% (restauration FR 2024, simplified). TODO in P3: verify rate with accountant (sur-place vs a-emporter + product type). Abandon button clears cart and returns to categories.
This commit is contained in:
parent
cd6e05c353
commit
c517b16569
2 changed files with 237 additions and 0 deletions
138
src/public/borne/assets/js/page-cart.js
Normal file
138
src/public/borne/assets/js/page-cart.js
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* page-cart.js — Shopping cart screen.
|
||||||
|
*
|
||||||
|
* Displays all cart lines with quantity controls and totals.
|
||||||
|
*
|
||||||
|
* TVA: 10% (taux normal restauration, France 2024 — simplification MVPp).
|
||||||
|
* TODO: verify exact applicable TVA rate with an accountant in P3.
|
||||||
|
* The real rate depends on sur-place vs a-emporter, alcohol content, etc.
|
||||||
|
*
|
||||||
|
* The total displayed is TTC (tax inclusive) because French consumer law
|
||||||
|
* requires prices shown to end-consumers to include all taxes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getCart, removeFromCart, updateQuantity, getTotalCents, clearCart, formatPrice } from './state.js';
|
||||||
|
import { refreshCartBadge } from './nav.js';
|
||||||
|
|
||||||
|
/* TVA rate used for display breakdown only — stored prices are already TTC */
|
||||||
|
const TVA_RATE = 0.10;
|
||||||
|
|
||||||
|
const cartList = document.getElementById('cart-list');
|
||||||
|
const emptyBlock = document.getElementById('cart-empty');
|
||||||
|
const summaryBlock= document.getElementById('cart-summary');
|
||||||
|
const totalTTC = document.getElementById('total-ttc');
|
||||||
|
const totalHT = document.getElementById('total-ht');
|
||||||
|
const totalTVA = document.getElementById('total-tva');
|
||||||
|
const payBtn = document.getElementById('pay-btn');
|
||||||
|
const abandonBtn = document.getElementById('abandon-btn');
|
||||||
|
|
||||||
|
function renderCart() {
|
||||||
|
const items = getCart();
|
||||||
|
refreshCartBadge();
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
cartList.innerHTML = '';
|
||||||
|
emptyBlock.hidden = false;
|
||||||
|
summaryBlock.hidden = true;
|
||||||
|
if (payBtn) payBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyBlock.hidden = false; /* keep structure; toggle via class */
|
||||||
|
emptyBlock.hidden = true;
|
||||||
|
summaryBlock.hidden = false;
|
||||||
|
if (payBtn) payBtn.disabled = false;
|
||||||
|
|
||||||
|
cartList.innerHTML = '';
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const lineTotalCents = item.prix_cents * item.quantite;
|
||||||
|
|
||||||
|
const row = document.createElement('li');
|
||||||
|
row.className = 'cart-line';
|
||||||
|
row.setAttribute('aria-label', `${item.libelle}, quantite ${item.quantite}`);
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<img
|
||||||
|
class="cart-line__image"
|
||||||
|
src="${item.image}"
|
||||||
|
alt="${item.libelle}"
|
||||||
|
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
|
||||||
|
>
|
||||||
|
<div class="cart-line__info">
|
||||||
|
<span class="cart-line__name">${item.libelle}</span>
|
||||||
|
<span class="cart-line__unit-price">${formatPrice(item.prix_cents)} / unite</span>
|
||||||
|
</div>
|
||||||
|
<div class="cart-line__qty" role="group" aria-label="Quantite de ${item.libelle}">
|
||||||
|
<button
|
||||||
|
class="qty-btn qty-btn--minus"
|
||||||
|
data-index="${index}"
|
||||||
|
aria-label="Diminuer la quantite de ${item.libelle}"
|
||||||
|
type="button"
|
||||||
|
>-</button>
|
||||||
|
<span class="qty-value" aria-live="polite">${item.quantite}</span>
|
||||||
|
<button
|
||||||
|
class="qty-btn qty-btn--plus"
|
||||||
|
data-index="${index}"
|
||||||
|
aria-label="Augmenter la quantite de ${item.libelle}"
|
||||||
|
type="button"
|
||||||
|
>+</button>
|
||||||
|
</div>
|
||||||
|
<span class="cart-line__total">${formatPrice(lineTotalCents)}</span>
|
||||||
|
<button
|
||||||
|
class="cart-line__remove"
|
||||||
|
data-index="${index}"
|
||||||
|
aria-label="Supprimer ${item.libelle} du panier"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<img src="assets/images/ui/trash.png" alt="" aria-hidden="true" width="24" height="24">
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
cartList.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Attach event listeners after render */
|
||||||
|
cartList.querySelectorAll('.qty-btn--minus').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
|
const cart = getCart();
|
||||||
|
updateQuantity(idx, cart[idx].quantite - 1);
|
||||||
|
renderCart();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cartList.querySelectorAll('.qty-btn--plus').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
|
const cart = getCart();
|
||||||
|
updateQuantity(idx, cart[idx].quantite + 1);
|
||||||
|
renderCart();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cartList.querySelectorAll('.cart-line__remove').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
|
removeFromCart(idx);
|
||||||
|
renderCart();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Update totals */
|
||||||
|
const ttcCents = getTotalCents();
|
||||||
|
/* Back-calculate HT from TTC (prices assumed to be TTC already) */
|
||||||
|
const htCents = Math.round(ttcCents / (1 + TVA_RATE));
|
||||||
|
const tvaCents = ttcCents - htCents;
|
||||||
|
|
||||||
|
if (totalTTC) totalTTC.textContent = formatPrice(ttcCents);
|
||||||
|
if (totalHT) totalHT.textContent = formatPrice(htCents);
|
||||||
|
if (totalTVA) totalTVA.textContent = formatPrice(tvaCents);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abandonBtn) {
|
||||||
|
abandonBtn.addEventListener('click', () => {
|
||||||
|
clearCart();
|
||||||
|
window.location.href = 'categories.html';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', renderCart);
|
||||||
99
src/public/borne/cart.html
Normal file
99
src/public/borne/cart.html
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<meta name="description" content="Wakdo - Votre panier">
|
||||||
|
<title>Wakdo - Panier</title>
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body class="cart-page">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
cart.html — Shopping cart.
|
||||||
|
page-cart.js renders all cart lines, handles qty controls and removal.
|
||||||
|
|
||||||
|
TVA: 10% (restauration France 2024 — simplified rate).
|
||||||
|
TODO: verify exact rate with accountant in P3 — actual rate depends
|
||||||
|
on sur-place vs a-emporter and product type (alcohol, etc.).
|
||||||
|
|
||||||
|
The stored prices are TTC. HT is back-calculated at display time only.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<header class="site-header">
|
||||||
|
<a
|
||||||
|
class="site-header__back"
|
||||||
|
href="categories.html"
|
||||||
|
aria-label="Continuer mes achats"
|
||||||
|
>
|
||||||
|
← Continuer
|
||||||
|
</a>
|
||||||
|
<img
|
||||||
|
class="site-header__logo"
|
||||||
|
src="assets/images/ui/logo.png"
|
||||||
|
alt="Wakdo"
|
||||||
|
>
|
||||||
|
<span class="mode-badge site-header__mode" data-mode-badge aria-label="Mode de consommation">Sur place</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="cart-main" aria-label="Votre panier">
|
||||||
|
|
||||||
|
<h1 class="cart-main__heading">Votre panier</h1>
|
||||||
|
|
||||||
|
<!-- Empty cart state -->
|
||||||
|
<div id="cart-empty" class="cart-empty" hidden>
|
||||||
|
<p class="cart-empty__message">Votre panier est vide.</p>
|
||||||
|
<a class="btn btn--secondary" href="categories.html">Decouvrir nos produits</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cart lines -->
|
||||||
|
<ul id="cart-list" class="cart-list" aria-label="Lignes du panier">
|
||||||
|
<!-- Filled by page-cart.js -->
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Order summary -->
|
||||||
|
<aside id="cart-summary" class="cart-summary" hidden aria-label="Recapitulatif de commande">
|
||||||
|
<div class="cart-summary__line">
|
||||||
|
<span>Total HT</span>
|
||||||
|
<span id="total-ht">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="cart-summary__line">
|
||||||
|
<!-- TVA 10% — taux restauration FR 2024 (simplifie, voir commentaire ci-dessus) -->
|
||||||
|
<span>TVA (10%)</span>
|
||||||
|
<span id="total-tva">—</span>
|
||||||
|
</div>
|
||||||
|
<div class="cart-summary__line cart-summary__line--total">
|
||||||
|
<span>Total TTC</span>
|
||||||
|
<strong id="total-ttc">—</strong>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="cart-actions">
|
||||||
|
<button
|
||||||
|
id="abandon-btn"
|
||||||
|
class="btn btn--secondary"
|
||||||
|
type="button"
|
||||||
|
aria-label="Abandonner la commande et retourner aux categories"
|
||||||
|
>
|
||||||
|
Abandonner
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
id="pay-btn"
|
||||||
|
class="btn btn--primary"
|
||||||
|
href="payment.html"
|
||||||
|
role="button"
|
||||||
|
aria-label="Passer au paiement"
|
||||||
|
aria-disabled="true"
|
||||||
|
>
|
||||||
|
Valider ma commande
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="assets/js/nav.js"></script>
|
||||||
|
<script type="module" src="assets/js/page-cart.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Reference in a new issue