Treuebonus-System: 10 abgeschlossene Bestellungen = 20% Rabatt auf einen frei wählbaren Artikel

Neue eigene Kiste "Treuebonus" im Kundenkonto (5. Nav-Tab, grüner Akzent)
zeigt den Fortschritt ("X von 10 Bestellungen") mit Fortschrittsbalken und
Restanzahl-Hinweis, nach Freischaltung eine Erfolgsmeldung mit Link zum
Warenkorb. Es zählen ausschließlich erfolgreich abgeschickte, bezahlte
Bestellungen (in Phase 1: jede erfolgreich abgeschlossene Demo-Bestellung
im Checkout) — abgebrochene Bestellungen zählen nie mit.

Sobald der Bonus aktiv ist, erscheint im Warenkorb ein Hinweis-Banner und
bei jedem Artikel eine Auswahl ("Für 20% Treuebonus auswählen") — es kann
immer nur EIN Artikel gewählt werden (Radiobutton-Verhalten über alle
Positionen hinweg). Der Rabatt fließt automatisch in Warenkorb- und
Checkout-Summe ein (neue treuebonusDiscount()-Funktion in scripts/cart.ts,
in totalDiscount() eingerechnet). Nach erfolgreichem Bestellabschluss wird
die Belohnung als eingelöst markiert und der Fortschrittszähler beginnt
wieder bei 0 — beides dauerhaft im Konto (localStorage, Phase 1) gespeichert
und eindeutig diesem Kundenkonto zugeordnet.

Neue Funktionen in scripts/account.ts: registriereAbgeschlosseneBestellung(),
setTreuebonusArtikel(), treuebonusEinloesen(), treueFortschritt(). Beide
Zustandsübergänge (Fortschritt zählen vs. Belohnung einlösen) end-to-end
über den echten Checkout-Ablauf getestet und verifiziert.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-03 15:03:48 +02:00
co-authored by Claude Sonnet 5
parent d7ba1b9bff
commit 0cb7d3ca65
16 changed files with 769 additions and 63 deletions
+51 -5
View File
@@ -21,6 +21,16 @@ const t = useTranslations(lang);
<a class="btn btn-primary" href="/en/shop/">{t.common.browseShop}</a>
</div>
{/* Treuebonus-Banner: nur sichtbar, wenn im Konto ein Bonus wartet (siehe scripts/account.ts)
— erklärt direkt hier, dass man unten bei einem Artikel "-20%" auswählen kann. */}
<div class="card loyalty-banner" id="loyalty-banner" style="display:none;">
<span class="loyalty-banner-icon" aria-hidden="true">🎁</span>
<div>
<strong>{t.cart.loyaltyBannerTitle}</strong>
<p>{t.cart.loyaltyBannerText}</p>
</div>
</div>
<div id="cart-content" class="cart-layout" style="display:none;">
<div class="card cart-items" id="cart-items"></div>
<aside class="card cart-summary">
@@ -29,6 +39,7 @@ const t = useTranslations(lang);
<div class="summary-row"><span>{t.cart.subtotal}</span><span id="sum-subtotal">0,00 €</span></div>
<div class="summary-row discount-row" id="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div>
<div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group">
@@ -66,18 +77,19 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove }}>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltySelectLabel: t.cart.loyaltySelectLabel, loyaltySelectedLabel: t.cart.loyaltySelectedLabel }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel };
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, istAusgewaehlt, setAusgewaehlt } from "../../../scripts/cart";
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt, setAusgewaehlt } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { getProduct, products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt";
import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scripts/account";
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel } = window.__cartVars;
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel } = window.__cartVars;
const landSelect = document.getElementById("cart-land");
function render() {
@@ -101,6 +113,14 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
return { item: i, produkt, zeilenpreis, mengenProzent };
});
// Treuebonus: nur relevant, wenn im Konto gerade eine Belohnung auf Einlösung wartet — dann
// darf hier EIN Artikel frei ausgewählt werden, auf den die -20% angewendet werden.
const konto = getAccount();
const treueAktiv = treueFortschritt(konto).aktiv;
const gewaehlterSlug = konto.treuebonusArtikelSlug;
const loyaltyBanner = document.getElementById("loyalty-banner");
if (loyaltyBanner) loyaltyBanner.style.display = treueAktiv ? "flex" : "none";
itemsEl.innerHTML = zeilen.map(({ item: i, produkt, zeilenpreis, mengenProzent }) => {
const foto = produkt?.bilder?.[0];
const thumb = foto
@@ -109,8 +129,12 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
const mengenBadge = mengenProzent > 0 ? `<div class="small mengenrabatt-badge">${mengenrabattTemplate.replace("__P__", mengenProzent)}</div>` : "";
const ausgewaehlt = istAusgewaehlt(i);
const keptBadge = !ausgewaehlt ? `<div class="small kept-in-cart-badge">🕓 ${keptInCartLabel}</div>` : "";
const istTreueartikel = gewaehlterSlug === i.slug;
const loyaltyControl = !treueAktiv ? "" : istTreueartikel
? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>`
: `<label class="loyalty-select"><input type="radio" name="loyalty-artikel" data-loyalty-slug="${i.slug}" /> ${loyaltySelectLabel}</label>`;
return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"}">
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
${thumb}
<div class="info">
@@ -118,6 +142,7 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
${mengenBadge}
${keptBadge}
${loyaltyControl}
</div>
<div class="qty-controls">
<button data-action="dec" data-slug="${i.slug}" aria-label=""></button>
@@ -130,6 +155,13 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
`;
}).join("");
itemsEl.querySelectorAll("[data-loyalty-slug]").forEach((el) => {
el.addEventListener("change", (e) => {
setTreuebonusArtikel(e.currentTarget.dataset.loyaltySlug);
render();
});
});
// Nur ausgewählte Artikel fließen in Rechnung/Zwischensumme/Versand/Checkout ein — abgewählte
// bleiben sichtbar im Warenkorb liegen (leicht abgedunkelt), ohne mitgekauft zu werden.
const ausgewaehlteZeilen = zeilen.filter(({ item: i }) => istAusgewaehlt(i));
@@ -184,6 +216,17 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
partnerStatus.textContent = partnerInput.dataset.invalid === "1" ? partnerCodeInvalid : "";
}
const loyaltyDiscountRow = document.getElementById("loyalty-discount-row");
const loyaltyDiscount = treuebonusDiscount();
if (loyaltyDiscountRow) {
if (loyaltyDiscount > 0) {
loyaltyDiscountRow.style.display = "flex";
document.getElementById("sum-loyalty-discount").textContent = "-" + formatPrice(loyaltyDiscount, cartLang);
} else {
loyaltyDiscountRow.style.display = "none";
}
}
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
@@ -232,5 +275,8 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
render();
window.addEventListener("cart:changed", render);
// Treuebonus-Auswahl liegt im Konto, nicht im Warenkorb — eigenes Event, damit die Anzeige
// auch reagiert, wenn sich NUR das Konto ändert (z.B. Bonus wird woanders freigeschaltet).
window.addEventListener("account:changed", render);
landSelect.addEventListener("change", render);
</script>