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
+15 -4
View File
@@ -173,19 +173,20 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/ch/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove }}>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/ch/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel };
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel };
</script>
<script type="module">
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, istAusgewaehlt } from "../../../scripts/cart";
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis } from "../../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel } = window.__checkoutVars;
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
@@ -202,6 +203,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
const discount = couponDiscount(subtotal);
const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount();
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total;
@@ -213,6 +215,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
}).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
`;
@@ -334,6 +337,14 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
alert(emptyCartAlert);
return;
}
// Treuebonus (siehe scripts/account.ts): Eine Bestellung zählt entweder für den NÄCHSTEN
// Durchlauf (registriereAbgeschlosseneBestellung) ODER löst eine bereits wartende Belohnung
// ein (treuebonusEinloesen) — nie beides gleichzeitig.
if (treuebonusDiscount() > 0) {
treuebonusEinloesen();
} else {
registriereAbgeschlosseneBestellung();
}
location.href = thankYouPath;
});
</script>
+67 -4
View File
@@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<span class="account-nav-icon" aria-hidden="true">👤</span>
<span class="account-nav-label">{t.accountDash.statData}</span>
</button>
<button type="button" class="account-nav-item account-nav-loyalty" data-tab="treuebonus">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🎁</span>
<span class="account-nav-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button>
</nav>
</div>
</section>
@@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
</div>
</section>
{/* Treuebonus: 10 gültige, abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel
(siehe scripts/account.ts). Fortschritt/Status kommen komplett aus dem Konto und werden per
JS befüllt (siehe Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="treuebonus">
<div class="container">
<h2>{t.accountDash.loyaltyTitle}</h2>
<div class="card loyalty-card">
<span class="loyalty-card-sparkle loyalty-card-sparkle-1" aria-hidden="true">✨</span>
<span class="loyalty-card-sparkle loyalty-card-sparkle-2" aria-hidden="true">✨</span>
<div class="loyalty-progress-bar"><div class="loyalty-progress-fill" id="loyalty-bar-fill"></div></div>
<p class="loyalty-progress-text" id="loyalty-progress-text"></p>
<p id="loyalty-status-text"></p>
<a class="btn btn-primary btn-sm" id="loyalty-cart-link" href="/warenkorb/" style="display:none; align-self:flex-start;">{t.accountDash.loyaltyGoToCart}</a>
<p class="small loyalty-explainer">{t.accountDash.loyaltyExplainer}</p>
</div>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels };
<script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels , loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -306,6 +330,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const wunschlisteSection = document.getElementById("wunschliste");
const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus");
const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -317,6 +342,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (bestellungenSection) bestellungenSection.style.display = "none";
if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none";
if (recentSection) recentSection.style.display = "none";
if (tab === null) {
@@ -334,6 +360,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return;
}
if (tab === "treuebonus") {
if (treuebonusSection) treuebonusSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0;
orderCards.forEach((card) => {
@@ -359,9 +390,9 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../../scripts/account";
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels } = (window as any).__accountDashVars;
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -403,6 +434,38 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderProfil();
window.addEventListener("account:changed", renderProfil);
// Treuebonus: Fortschritt/Status live aus dem Konto rendern (Nav-Kachel + Inhaltsbereich).
function renderLoyalty() {
const f = treueFortschritt(getAccount());
const navValue = document.getElementById("loyalty-nav-value");
if (navValue) navValue.textContent = `${f.aktuelle}/${f.ziel}`;
document.querySelector(".account-nav-loyalty")?.classList.toggle("is-ready", f.aktiv);
const fill = document.getElementById("loyalty-bar-fill") as HTMLElement | null;
if (fill) fill.style.width = `${Math.min(100, (f.aktuelle / f.ziel) * 100)}%`;
const progressText = document.getElementById("loyalty-progress-text");
if (progressText) {
progressText.textContent = loyaltyProgressTemplate
.replace("{cur}", String(f.aktuelle))
.replace("{ziel}", String(f.ziel));
}
const statusText = document.getElementById("loyalty-status-text");
const cartLink = document.getElementById("loyalty-cart-link") as HTMLElement | null;
if (statusText) {
if (f.aktiv) {
statusText.innerHTML = `<strong>${loyaltyUnlockedTitle}</strong><br />${loyaltyUnlockedText}`;
if (cartLink) cartLink.style.display = "inline-flex";
} else {
statusText.textContent = f.fehlende === 1 ? loyaltyRemainingOne : loyaltyRemainingOther.replace("{n}", String(f.fehlende));
if (cartLink) cartLink.style.display = "none";
}
}
}
renderLoyalty();
window.addEventListener("account:changed", renderLoyalty);
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+51 -5
View File
@@ -21,6 +21,16 @@ const t = useTranslations(lang);
<a class="btn btn-primary" href="/ch/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>
+17 -4
View File
@@ -173,22 +173,23 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove }}>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount }}>
// WICHTIG: define:vars-Skripte werden von Astro in ein IIFE gepackt (kein ES-Modul) — echte
// `import`-Anweisungen würden hier zur Laufzeit mit "Cannot use import statement outside a
// module" fehlschlagen. Deshalb hier nur die vom Server gerenderten Werte auf window ablegen,
// die eigentliche Logik läuft im separaten <script type="module"> darunter.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel };
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel };
</script>
<script type="module">
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, istAusgewaehlt } from "../../scripts/cart";
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt } from "../../scripts/cart";
import { formatPrice } from "../../i18n/format";
import { products } from "../../data/products";
import { berechneVersandkosten } from "../../data/versand";
import { effektiverZeilenpreis } from "../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../scripts/account";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel } = window.__checkoutVars;
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
@@ -205,6 +206,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbi
const discount = couponDiscount(subtotal);
const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount();
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total;
@@ -216,6 +218,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbi
}).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
`;
@@ -337,6 +340,16 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbi
alert(emptyCartAlert);
return;
}
// Treuebonus (siehe scripts/account.ts): Eine Bestellung zählt entweder für den NÄCHSTEN
// Durchlauf (registriereAbgeschlosseneBestellung) ODER löst eine bereits wartende Belohnung
// ein (treuebonusEinloesen) — nie beides gleichzeitig. Nur eine erfolgreich abgeschickte,
// vollständig bezahlte Bestellung zählt hier; abgebrochene/nicht abgeschickte Bestellungen
// rufen diese Funktionen nie auf.
if (treuebonusDiscount() > 0) {
treuebonusEinloesen();
} else {
registriereAbgeschlosseneBestellung();
}
location.href = thankYouPath;
});
</script>
+15 -4
View File
@@ -173,19 +173,20 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/en/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove }}>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/en/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel };
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel };
</script>
<script type="module">
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, istAusgewaehlt } from "../../../scripts/cart";
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis } from "../../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel } = window.__checkoutVars;
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
@@ -202,6 +203,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
const discount = couponDiscount(subtotal);
const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount();
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total;
@@ -213,6 +215,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
}).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
`;
@@ -334,6 +337,14 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
alert(emptyCartAlert);
return;
}
// Treuebonus (siehe scripts/account.ts): Eine Bestellung zählt entweder für den NÄCHSTEN
// Durchlauf (registriereAbgeschlosseneBestellung) ODER löst eine bereits wartende Belohnung
// ein (treuebonusEinloesen) — nie beides gleichzeitig.
if (treuebonusDiscount() > 0) {
treuebonusEinloesen();
} else {
registriereAbgeschlosseneBestellung();
}
location.href = thankYouPath;
});
</script>
+67 -4
View File
@@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<span class="account-nav-icon" aria-hidden="true">👤</span>
<span class="account-nav-label">{t.accountDash.statData}</span>
</button>
<button type="button" class="account-nav-item account-nav-loyalty" data-tab="treuebonus">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🎁</span>
<span class="account-nav-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button>
</nav>
</div>
</section>
@@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
</div>
</section>
{/* Treuebonus: 10 gültige, abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel
(siehe scripts/account.ts). Fortschritt/Status kommen komplett aus dem Konto und werden per
JS befüllt (siehe Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="treuebonus">
<div class="container">
<h2>{t.accountDash.loyaltyTitle}</h2>
<div class="card loyalty-card">
<span class="loyalty-card-sparkle loyalty-card-sparkle-1" aria-hidden="true">✨</span>
<span class="loyalty-card-sparkle loyalty-card-sparkle-2" aria-hidden="true">✨</span>
<div class="loyalty-progress-bar"><div class="loyalty-progress-fill" id="loyalty-bar-fill"></div></div>
<p class="loyalty-progress-text" id="loyalty-progress-text"></p>
<p id="loyalty-status-text"></p>
<a class="btn btn-primary btn-sm" id="loyalty-cart-link" href="/warenkorb/" style="display:none; align-self:flex-start;">{t.accountDash.loyaltyGoToCart}</a>
<p class="small loyalty-explainer">{t.accountDash.loyaltyExplainer}</p>
</div>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels };
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels , loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -306,6 +330,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const wunschlisteSection = document.getElementById("wunschliste");
const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus");
const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -317,6 +342,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (bestellungenSection) bestellungenSection.style.display = "none";
if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none";
if (recentSection) recentSection.style.display = "none";
if (tab === null) {
@@ -334,6 +360,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return;
}
if (tab === "treuebonus") {
if (treuebonusSection) treuebonusSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0;
orderCards.forEach((card) => {
@@ -359,9 +390,9 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../../scripts/account";
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels } = (window as any).__accountDashVars;
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -403,6 +434,38 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderProfil();
window.addEventListener("account:changed", renderProfil);
// Treuebonus: Fortschritt/Status live aus dem Konto rendern (Nav-Kachel + Inhaltsbereich).
function renderLoyalty() {
const f = treueFortschritt(getAccount());
const navValue = document.getElementById("loyalty-nav-value");
if (navValue) navValue.textContent = `${f.aktuelle}/${f.ziel}`;
document.querySelector(".account-nav-loyalty")?.classList.toggle("is-ready", f.aktiv);
const fill = document.getElementById("loyalty-bar-fill") as HTMLElement | null;
if (fill) fill.style.width = `${Math.min(100, (f.aktuelle / f.ziel) * 100)}%`;
const progressText = document.getElementById("loyalty-progress-text");
if (progressText) {
progressText.textContent = loyaltyProgressTemplate
.replace("{cur}", String(f.aktuelle))
.replace("{ziel}", String(f.ziel));
}
const statusText = document.getElementById("loyalty-status-text");
const cartLink = document.getElementById("loyalty-cart-link") as HTMLElement | null;
if (statusText) {
if (f.aktiv) {
statusText.innerHTML = `<strong>${loyaltyUnlockedTitle}</strong><br />${loyaltyUnlockedText}`;
if (cartLink) cartLink.style.display = "inline-flex";
} else {
statusText.textContent = f.fehlende === 1 ? loyaltyRemainingOne : loyaltyRemainingOther.replace("{n}", String(f.fehlende));
if (cartLink) cartLink.style.display = "none";
}
}
}
renderLoyalty();
window.addEventListener("account:changed", renderLoyalty);
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+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>
+15 -4
View File
@@ -173,19 +173,20 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/fr/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove }}>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/fr/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel };
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel };
</script>
<script type="module">
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, istAusgewaehlt } from "../../../scripts/cart";
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis } from "../../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel } = window.__checkoutVars;
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
@@ -202,6 +203,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
const discount = couponDiscount(subtotal);
const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount();
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total;
@@ -213,6 +215,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
}).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
`;
@@ -334,6 +337,14 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
alert(emptyCartAlert);
return;
}
// Treuebonus (siehe scripts/account.ts): Eine Bestellung zählt entweder für den NÄCHSTEN
// Durchlauf (registriereAbgeschlosseneBestellung) ODER löst eine bereits wartende Belohnung
// ein (treuebonusEinloesen) — nie beides gleichzeitig.
if (treuebonusDiscount() > 0) {
treuebonusEinloesen();
} else {
registriereAbgeschlosseneBestellung();
}
location.href = thankYouPath;
});
</script>
+67 -4
View File
@@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<span class="account-nav-icon" aria-hidden="true">👤</span>
<span class="account-nav-label">{t.accountDash.statData}</span>
</button>
<button type="button" class="account-nav-item account-nav-loyalty" data-tab="treuebonus">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🎁</span>
<span class="account-nav-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button>
</nav>
</div>
</section>
@@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
</div>
</section>
{/* Treuebonus: 10 gültige, abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel
(siehe scripts/account.ts). Fortschritt/Status kommen komplett aus dem Konto und werden per
JS befüllt (siehe Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="treuebonus">
<div class="container">
<h2>{t.accountDash.loyaltyTitle}</h2>
<div class="card loyalty-card">
<span class="loyalty-card-sparkle loyalty-card-sparkle-1" aria-hidden="true">✨</span>
<span class="loyalty-card-sparkle loyalty-card-sparkle-2" aria-hidden="true">✨</span>
<div class="loyalty-progress-bar"><div class="loyalty-progress-fill" id="loyalty-bar-fill"></div></div>
<p class="loyalty-progress-text" id="loyalty-progress-text"></p>
<p id="loyalty-status-text"></p>
<a class="btn btn-primary btn-sm" id="loyalty-cart-link" href="/warenkorb/" style="display:none; align-self:flex-start;">{t.accountDash.loyaltyGoToCart}</a>
<p class="small loyalty-explainer">{t.accountDash.loyaltyExplainer}</p>
</div>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels };
<script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels , loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -306,6 +330,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const wunschlisteSection = document.getElementById("wunschliste");
const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus");
const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -317,6 +342,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (bestellungenSection) bestellungenSection.style.display = "none";
if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none";
if (recentSection) recentSection.style.display = "none";
if (tab === null) {
@@ -334,6 +360,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return;
}
if (tab === "treuebonus") {
if (treuebonusSection) treuebonusSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0;
orderCards.forEach((card) => {
@@ -359,9 +390,9 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../../scripts/account";
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels } = (window as any).__accountDashVars;
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -403,6 +434,38 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderProfil();
window.addEventListener("account:changed", renderProfil);
// Treuebonus: Fortschritt/Status live aus dem Konto rendern (Nav-Kachel + Inhaltsbereich).
function renderLoyalty() {
const f = treueFortschritt(getAccount());
const navValue = document.getElementById("loyalty-nav-value");
if (navValue) navValue.textContent = `${f.aktuelle}/${f.ziel}`;
document.querySelector(".account-nav-loyalty")?.classList.toggle("is-ready", f.aktiv);
const fill = document.getElementById("loyalty-bar-fill") as HTMLElement | null;
if (fill) fill.style.width = `${Math.min(100, (f.aktuelle / f.ziel) * 100)}%`;
const progressText = document.getElementById("loyalty-progress-text");
if (progressText) {
progressText.textContent = loyaltyProgressTemplate
.replace("{cur}", String(f.aktuelle))
.replace("{ziel}", String(f.ziel));
}
const statusText = document.getElementById("loyalty-status-text");
const cartLink = document.getElementById("loyalty-cart-link") as HTMLElement | null;
if (statusText) {
if (f.aktiv) {
statusText.innerHTML = `<strong>${loyaltyUnlockedTitle}</strong><br />${loyaltyUnlockedText}`;
if (cartLink) cartLink.style.display = "inline-flex";
} else {
statusText.textContent = f.fehlende === 1 ? loyaltyRemainingOne : loyaltyRemainingOther.replace("{n}", String(f.fehlende));
if (cartLink) cartLink.style.display = "none";
}
}
}
renderLoyalty();
window.addEventListener("account:changed", renderLoyalty);
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+51 -5
View File
@@ -21,6 +21,16 @@ const t = useTranslations(lang);
<a class="btn btn-primary" href="/fr/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>
+67 -4
View File
@@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<span class="account-nav-icon" aria-hidden="true">👤</span>
<span class="account-nav-label">{t.accountDash.statData}</span>
</button>
<button type="button" class="account-nav-item account-nav-loyalty" data-tab="treuebonus">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🎁</span>
<span class="account-nav-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button>
</nav>
</div>
</section>
@@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
</div>
</section>
{/* Treuebonus: 10 gültige, abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel
(siehe scripts/account.ts). Fortschritt/Status kommen komplett aus dem Konto und werden per
JS befüllt (siehe Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="treuebonus">
<div class="container">
<h2>{t.accountDash.loyaltyTitle}</h2>
<div class="card loyalty-card">
<span class="loyalty-card-sparkle loyalty-card-sparkle-1" aria-hidden="true">✨</span>
<span class="loyalty-card-sparkle loyalty-card-sparkle-2" aria-hidden="true">✨</span>
<div class="loyalty-progress-bar"><div class="loyalty-progress-fill" id="loyalty-bar-fill"></div></div>
<p class="loyalty-progress-text" id="loyalty-progress-text"></p>
<p id="loyalty-status-text"></p>
<a class="btn btn-primary btn-sm" id="loyalty-cart-link" href="/warenkorb/" style="display:none; align-self:flex-start;">{t.accountDash.loyaltyGoToCart}</a>
<p class="small loyalty-explainer">{t.accountDash.loyaltyExplainer}</p>
</div>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels };
<script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -306,6 +330,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const wunschlisteSection = document.getElementById("wunschliste");
const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus");
const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -317,6 +342,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (bestellungenSection) bestellungenSection.style.display = "none";
if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none";
if (recentSection) recentSection.style.display = "none";
if (tab === null) {
@@ -334,6 +360,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return;
}
if (tab === "treuebonus") {
if (treuebonusSection) treuebonusSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0;
orderCards.forEach((card) => {
@@ -359,9 +390,9 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName } from "../../scripts/account";
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt } from "../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels } = (window as any).__accountDashVars;
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -403,6 +434,38 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderProfil();
window.addEventListener("account:changed", renderProfil);
// Treuebonus: Fortschritt/Status live aus dem Konto rendern (Nav-Kachel + Inhaltsbereich).
function renderLoyalty() {
const f = treueFortschritt(getAccount());
const navValue = document.getElementById("loyalty-nav-value");
if (navValue) navValue.textContent = `${f.aktuelle}/${f.ziel}`;
document.querySelector(".account-nav-loyalty")?.classList.toggle("is-ready", f.aktiv);
const fill = document.getElementById("loyalty-bar-fill") as HTMLElement | null;
if (fill) fill.style.width = `${Math.min(100, (f.aktuelle / f.ziel) * 100)}%`;
const progressText = document.getElementById("loyalty-progress-text");
if (progressText) {
progressText.textContent = loyaltyProgressTemplate
.replace("{cur}", String(f.aktuelle))
.replace("{ziel}", String(f.ziel));
}
const statusText = document.getElementById("loyalty-status-text");
const cartLink = document.getElementById("loyalty-cart-link") as HTMLElement | null;
if (statusText) {
if (f.aktiv) {
statusText.innerHTML = `<strong>${loyaltyUnlockedTitle}</strong><br />${loyaltyUnlockedText}`;
if (cartLink) cartLink.style.display = "inline-flex";
} else {
statusText.textContent = f.fehlende === 1 ? loyaltyRemainingOne : loyaltyRemainingOther.replace("{n}", String(f.fehlende));
if (cartLink) cartLink.style.display = "none";
}
}
}
renderLoyalty();
window.addEventListener("account:changed", renderLoyalty);
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+51 -5
View File
@@ -21,6 +21,16 @@ const t = useTranslations(lang);
<a class="btn btn-primary" href="/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>