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:
co-authored by
Claude Sonnet 5
parent
d7ba1b9bff
commit
0cb7d3ca65
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user