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
+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;