Treuebonus-System: 10 abgeschlossene Bestellungen = 20% Rabatt auf einen frei wählbaren Artikel
Neue eigene Kiste "Treuebonus" im Kundenkonto (5. Nav-Tab, grüner Akzent)
zeigt den Fortschritt ("X von 10 Bestellungen") mit Fortschrittsbalken und
Restanzahl-Hinweis, nach Freischaltung eine Erfolgsmeldung mit Link zum
Warenkorb. Es zählen ausschließlich erfolgreich abgeschickte, bezahlte
Bestellungen (in Phase 1: jede erfolgreich abgeschlossene Demo-Bestellung
im Checkout) — abgebrochene Bestellungen zählen nie mit.
Sobald der Bonus aktiv ist, erscheint im Warenkorb ein Hinweis-Banner und
bei jedem Artikel eine Auswahl ("Für 20% Treuebonus auswählen") — es kann
immer nur EIN Artikel gewählt werden (Radiobutton-Verhalten über alle
Positionen hinweg). Der Rabatt fließt automatisch in Warenkorb- und
Checkout-Summe ein (neue treuebonusDiscount()-Funktion in scripts/cart.ts,
in totalDiscount() eingerechnet). Nach erfolgreichem Bestellabschluss wird
die Belohnung als eingelöst markiert und der Fortschrittszähler beginnt
wieder bei 0 — beides dauerhaft im Konto (localStorage, Phase 1) gespeichert
und eindeutig diesem Kundenkonto zugeordnet.
Neue Funktionen in scripts/account.ts: registriereAbgeschlosseneBestellung(),
setTreuebonusArtikel(), treuebonusEinloesen(), treueFortschritt(). Beide
Zustandsübergänge (Fortschritt zählen vs. Belohnung einlösen) end-to-end
über den echten Checkout-Ablauf getestet und verifiziert.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d7ba1b9bff
commit
0cb7d3ca65
+56
-4
@@ -175,6 +175,11 @@ export const ui = {
|
|||||||
partnerDiscount: "🤝 Partner-Rabatt",
|
partnerDiscount: "🤝 Partner-Rabatt",
|
||||||
partnerCodeLabel: "🤝 Partner-Rabattcode", partnerCodePlaceholder: "Code eingeben", partnerCodeApply: "Einlösen", partnerCodeRemove: "Entfernen",
|
partnerCodeLabel: "🤝 Partner-Rabattcode", partnerCodePlaceholder: "Code eingeben", partnerCodeApply: "Einlösen", partnerCodeRemove: "Entfernen",
|
||||||
partnerCodeInvalid: "Code ungültig oder abgelaufen.", partnerCodeApplied: (code: string) => `Partner-Code „${code}" eingelöst`,
|
partnerCodeInvalid: "Code ungültig oder abgelaufen.", partnerCodeApplied: (code: string) => `Partner-Code „${code}" eingelöst`,
|
||||||
|
loyaltyDiscount: "🎁 Treuebonus (-20 %)",
|
||||||
|
loyaltyBannerTitle: "🎁 Dein Treuebonus ist bereit!",
|
||||||
|
loyaltyBannerText: "Wähle unten den Artikel aus, auf den du 20 % Rabatt erhältst.",
|
||||||
|
loyaltySelectLabel: "Für 20 % Treuebonus auswählen",
|
||||||
|
loyaltySelectedLabel: "🎁 -20 % Treuebonus angewendet",
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
eyebrow: "Checkout", title: "Bestellung abschließen",
|
eyebrow: "Checkout", title: "Bestellung abschließen",
|
||||||
@@ -224,7 +229,15 @@ export const ui = {
|
|||||||
notLoggedIn: "Du bist nicht angemeldet — du wirst zur Anmeldung weitergeleitet …",
|
notLoggedIn: "Du bist nicht angemeldet — du wirst zur Anmeldung weitergeleitet …",
|
||||||
logout: "Abmelden",
|
logout: "Abmelden",
|
||||||
navLabel: "Kontobereich-Navigation",
|
navLabel: "Kontobereich-Navigation",
|
||||||
statOrders: "Bestellungen", statWishlist: "Wunschliste", statOpen: "Offene Sendungen", statData: "Meine Daten",
|
statOrders: "Bestellungen", statWishlist: "Wunschliste", statOpen: "Offene Sendungen", statData: "Meine Daten", statLoyalty: "Treuebonus",
|
||||||
|
loyaltyTitle: "🎁 Mein Treuebonus",
|
||||||
|
loyaltyProgressTemplate: "Dein Bestellfortschritt: {cur} von {ziel} Bestellungen",
|
||||||
|
loyaltyRemainingOne: "Dir fehlt noch 1 Bestellung, bis du 20 % Rabatt auf einen Artikel deiner Wahl erhältst.",
|
||||||
|
loyaltyRemainingOther: "Dir fehlen noch {n} Bestellungen, bis du 20 % Rabatt auf einen Artikel deiner Wahl erhältst.",
|
||||||
|
loyaltyUnlockedTitle: "🎉 Treuebonus freigeschaltet",
|
||||||
|
loyaltyUnlockedText: "Du hast zehn Bestellungen abgeschlossen und erhältst bei deiner nächsten Bestellung 20 % Rabatt auf einen Artikel deiner Wahl.",
|
||||||
|
loyaltyGoToCart: "Artikel im Warenkorb auswählen →",
|
||||||
|
loyaltyExplainer: "Es zählen nur vollständig abgeschlossene und bezahlte Bestellungen. Nach der Einlösung beginnt dein Fortschritt wieder bei null.",
|
||||||
noOpenOrders: "Aktuell keine offenen Sendungen — alle Bestellungen sind schon angekommen. 🎉",
|
noOpenOrders: "Aktuell keine offenen Sendungen — alle Bestellungen sind schon angekommen. 🎉",
|
||||||
noArrivedOrders: "Noch keine angekommenen Bestellungen — hier erscheinen deine Bestellungen, sobald sie geliefert wurden.",
|
noArrivedOrders: "Noch keine angekommenen Bestellungen — hier erscheinen deine Bestellungen, sobald sie geliefert wurden.",
|
||||||
ordersTitle: "📦 Meine Bestellungen",
|
ordersTitle: "📦 Meine Bestellungen",
|
||||||
@@ -413,6 +426,11 @@ export const ui = {
|
|||||||
partnerDiscount: "🤝 Partner discount",
|
partnerDiscount: "🤝 Partner discount",
|
||||||
partnerCodeLabel: "🤝 Partner discount code", partnerCodePlaceholder: "Enter code", partnerCodeApply: "Apply", partnerCodeRemove: "Remove",
|
partnerCodeLabel: "🤝 Partner discount code", partnerCodePlaceholder: "Enter code", partnerCodeApply: "Apply", partnerCodeRemove: "Remove",
|
||||||
partnerCodeInvalid: "Code invalid or expired.", partnerCodeApplied: (code: string) => `Partner code "${code}" applied`,
|
partnerCodeInvalid: "Code invalid or expired.", partnerCodeApplied: (code: string) => `Partner code "${code}" applied`,
|
||||||
|
loyaltyDiscount: "🎁 Loyalty reward (-20%)",
|
||||||
|
loyaltyBannerTitle: "🎁 Your loyalty reward is ready!",
|
||||||
|
loyaltyBannerText: "Pick the item below that you'd like 20% off.",
|
||||||
|
loyaltySelectLabel: "Select for 20% loyalty reward",
|
||||||
|
loyaltySelectedLabel: "🎁 -20% loyalty reward applied",
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
eyebrow: "Checkout", title: "Complete your order",
|
eyebrow: "Checkout", title: "Complete your order",
|
||||||
@@ -462,7 +480,15 @@ export const ui = {
|
|||||||
notLoggedIn: "You're not signed in — redirecting you to sign in …",
|
notLoggedIn: "You're not signed in — redirecting you to sign in …",
|
||||||
logout: "Sign out",
|
logout: "Sign out",
|
||||||
navLabel: "Account section navigation",
|
navLabel: "Account section navigation",
|
||||||
statOrders: "Orders", statWishlist: "Wishlist", statOpen: "Open shipments", statData: "My details",
|
statOrders: "Orders", statWishlist: "Wishlist", statOpen: "Open shipments", statData: "My details", statLoyalty: "Loyalty reward",
|
||||||
|
loyaltyTitle: "🎁 My loyalty reward",
|
||||||
|
loyaltyProgressTemplate: "Your order progress: {cur} of {ziel} orders",
|
||||||
|
loyaltyRemainingOne: "You need 1 more order to get 20% off an item of your choice.",
|
||||||
|
loyaltyRemainingOther: "You need {n} more orders to get 20% off an item of your choice.",
|
||||||
|
loyaltyUnlockedTitle: "🎉 Loyalty reward unlocked",
|
||||||
|
loyaltyUnlockedText: "You've completed ten orders and get 20% off an item of your choice on your next order.",
|
||||||
|
loyaltyGoToCart: "Choose item in cart →",
|
||||||
|
loyaltyExplainer: "Only fully completed and paid orders count. After redemption your progress starts again from zero.",
|
||||||
noOpenOrders: "No open shipments right now — every order has already arrived. 🎉",
|
noOpenOrders: "No open shipments right now — every order has already arrived. 🎉",
|
||||||
noArrivedOrders: "No delivered orders yet — your orders will appear here once they've arrived.",
|
noArrivedOrders: "No delivered orders yet — your orders will appear here once they've arrived.",
|
||||||
ordersTitle: "📦 My orders",
|
ordersTitle: "📦 My orders",
|
||||||
@@ -651,6 +677,11 @@ export const ui = {
|
|||||||
partnerDiscount: "🤝 Partner-Rabatt",
|
partnerDiscount: "🤝 Partner-Rabatt",
|
||||||
partnerCodeLabel: "🤝 Partner-Rabattcode", partnerCodePlaceholder: "Code igäh", partnerCodeApply: "Ilöse", partnerCodeRemove: "Ewägmache",
|
partnerCodeLabel: "🤝 Partner-Rabattcode", partnerCodePlaceholder: "Code igäh", partnerCodeApply: "Ilöse", partnerCodeRemove: "Ewägmache",
|
||||||
partnerCodeInvalid: "Code ungültig oder abgloffe.", partnerCodeApplied: (code: string) => `Partner-Code „${code}" iglöst`,
|
partnerCodeInvalid: "Code ungültig oder abgloffe.", partnerCodeApplied: (code: string) => `Partner-Code „${code}" iglöst`,
|
||||||
|
loyaltyDiscount: "🎁 Treuebonus (-20 %)",
|
||||||
|
loyaltyBannerTitle: "🎁 Din Treuebonus isch parat!",
|
||||||
|
loyaltyBannerText: "Wähl unde dr Artikel us, uf dä du 20 % Rabatt übercho.",
|
||||||
|
loyaltySelectLabel: "Für 20 % Treuebonus uswähle",
|
||||||
|
loyaltySelectedLabel: "🎁 -20 % Treuebonus aagwändet",
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
eyebrow: "Checkout", title: "Bschtellig abschliesse",
|
eyebrow: "Checkout", title: "Bschtellig abschliesse",
|
||||||
@@ -700,7 +731,15 @@ export const ui = {
|
|||||||
notLoggedIn: "Du bisch nid aagmäldet — du wirsch zur Aamäldig wytergleitet …",
|
notLoggedIn: "Du bisch nid aagmäldet — du wirsch zur Aamäldig wytergleitet …",
|
||||||
logout: "Abmälde",
|
logout: "Abmälde",
|
||||||
navLabel: "Kontobereich-Navigation",
|
navLabel: "Kontobereich-Navigation",
|
||||||
statOrders: "Bstellige", statWishlist: "Wunschlischte", statOpen: "Offni Sändige", statData: "Mini Date",
|
statOrders: "Bstellige", statWishlist: "Wunschlischte", statOpen: "Offni Sändige", statData: "Mini Date", statLoyalty: "Treuebonus",
|
||||||
|
loyaltyTitle: "🎁 Mis Treuebonus",
|
||||||
|
loyaltyProgressTemplate: "Din Bstellfortschritt: {cur} vo {ziel} Bstellige",
|
||||||
|
loyaltyRemainingOne: "Dir fehlt no 1 Bstellig, bis du 20 % Rabatt uf en Artikel dinere Wahl übercho.",
|
||||||
|
loyaltyRemainingOther: "Dir fehle no {n} Bstellige, bis du 20 % Rabatt uf en Artikel dinere Wahl übercho.",
|
||||||
|
loyaltyUnlockedTitle: "🎉 Treuebonus freigschaltet",
|
||||||
|
loyaltyUnlockedText: "Du hesch zäh Bstellige abgschlosse und übercho bi dinere nächschte Bstellig 20 % Rabatt uf en Artikel dinere Wahl.",
|
||||||
|
loyaltyGoToCart: "Artikel im Warenchorb uswähle →",
|
||||||
|
loyaltyExplainer: "Es zelle nume vollständig abgschlosseni und bezahlti Bstellige. Nach dr Ilösig fangt din Fortschritt wieder bi null a.",
|
||||||
noOpenOrders: "Momentan kei offni Sändige — alli Bstellige sind scho aacho. 🎉",
|
noOpenOrders: "Momentan kei offni Sändige — alli Bstellige sind scho aacho. 🎉",
|
||||||
noArrivedOrders: "No kei aacho Bstellige — da erschine dini Bstellige, sobald sie glifert worde sind.",
|
noArrivedOrders: "No kei aacho Bstellige — da erschine dini Bstellige, sobald sie glifert worde sind.",
|
||||||
ordersTitle: "📦 Mini Bstellige",
|
ordersTitle: "📦 Mini Bstellige",
|
||||||
@@ -889,6 +928,11 @@ export const ui = {
|
|||||||
partnerDiscount: "🤝 Remise partenaire",
|
partnerDiscount: "🤝 Remise partenaire",
|
||||||
partnerCodeLabel: "🤝 Code partenaire", partnerCodePlaceholder: "Entrer le code", partnerCodeApply: "Valider", partnerCodeRemove: "Retirer",
|
partnerCodeLabel: "🤝 Code partenaire", partnerCodePlaceholder: "Entrer le code", partnerCodeApply: "Valider", partnerCodeRemove: "Retirer",
|
||||||
partnerCodeInvalid: "Code invalide ou expiré.", partnerCodeApplied: (code: string) => `Code partenaire « ${code} » appliqué`,
|
partnerCodeInvalid: "Code invalide ou expiré.", partnerCodeApplied: (code: string) => `Code partenaire « ${code} » appliqué`,
|
||||||
|
loyaltyDiscount: "🎁 Récompense fidélité (-20 %)",
|
||||||
|
loyaltyBannerTitle: "🎁 Votre récompense fidélité est prête !",
|
||||||
|
loyaltyBannerText: "Choisissez ci-dessous l'article sur lequel vous voulez -20 %.",
|
||||||
|
loyaltySelectLabel: "Choisir pour la récompense fidélité de 20 %",
|
||||||
|
loyaltySelectedLabel: "🎁 Récompense fidélité de -20 % appliquée",
|
||||||
},
|
},
|
||||||
checkout: {
|
checkout: {
|
||||||
eyebrow: "Paiement", title: "Finaliser la commande",
|
eyebrow: "Paiement", title: "Finaliser la commande",
|
||||||
@@ -938,7 +982,15 @@ export const ui = {
|
|||||||
notLoggedIn: "Vous n'êtes pas connecté(e) — redirection vers la connexion …",
|
notLoggedIn: "Vous n'êtes pas connecté(e) — redirection vers la connexion …",
|
||||||
logout: "Se déconnecter",
|
logout: "Se déconnecter",
|
||||||
navLabel: "Navigation de l'espace client",
|
navLabel: "Navigation de l'espace client",
|
||||||
statOrders: "Commandes", statWishlist: "Liste de souhaits", statOpen: "Envois en cours", statData: "Mes données",
|
statOrders: "Commandes", statWishlist: "Liste de souhaits", statOpen: "Envois en cours", statData: "Mes données", statLoyalty: "Récompense",
|
||||||
|
loyaltyTitle: "🎁 Ma récompense fidélité",
|
||||||
|
loyaltyProgressTemplate: "Votre progression : {cur} sur {ziel} commandes",
|
||||||
|
loyaltyRemainingOne: "Il vous manque encore 1 commande pour obtenir 20 % de réduction sur l'article de votre choix.",
|
||||||
|
loyaltyRemainingOther: "Il vous manque encore {n} commandes pour obtenir 20 % de réduction sur l'article de votre choix.",
|
||||||
|
loyaltyUnlockedTitle: "🎉 Récompense fidélité débloquée",
|
||||||
|
loyaltyUnlockedText: "Vous avez validé dix commandes et bénéficiez de 20 % de réduction sur l'article de votre choix lors de votre prochaine commande.",
|
||||||
|
loyaltyGoToCart: "Choisir l'article dans le panier →",
|
||||||
|
loyaltyExplainer: "Seules les commandes entièrement finalisées et payées comptent. Après utilisation, votre progression repart de zéro.",
|
||||||
noOpenOrders: "Aucun envoi en cours pour le moment — toutes les commandes sont déjà arrivées. 🎉",
|
noOpenOrders: "Aucun envoi en cours pour le moment — toutes les commandes sont déjà arrivées. 🎉",
|
||||||
noArrivedOrders: "Aucune commande livrée pour le moment — vos commandes apparaîtront ici dès qu'elles seront arrivées.",
|
noArrivedOrders: "Aucune commande livrée pour le moment — vos commandes apparaîtront ici dès qu'elles seront arrivées.",
|
||||||
ordersTitle: "📦 Mes commandes",
|
ordersTitle: "📦 Mes commandes",
|
||||||
|
|||||||
@@ -173,19 +173,20 @@ const t = useTranslations(lang);
|
|||||||
</section>
|
</section>
|
||||||
</Layout>
|
</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.
|
// 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>
|
||||||
<script type="module">
|
<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 { formatPrice } from "../../../i18n/format";
|
||||||
import { products } from "../../../data/products";
|
import { products } from "../../../data/products";
|
||||||
import { berechneVersandkosten } from "../../../data/versand";
|
import { berechneVersandkosten } from "../../../data/versand";
|
||||||
import { effektiverZeilenpreis } from "../../../data/rabatt";
|
import { effektiverZeilenpreis } from "../../../data/rabatt";
|
||||||
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
|
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 summary = document.getElementById("checkout-summary");
|
||||||
const landSelect = document.getElementById("land");
|
const landSelect = document.getElementById("land");
|
||||||
@@ -202,6 +203,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
|
|||||||
const discount = couponDiscount(subtotal);
|
const discount = couponDiscount(subtotal);
|
||||||
const partnerCode = appliedPartnerCode();
|
const partnerCode = appliedPartnerCode();
|
||||||
const partnerDiscount = partnerCodeDiscount(subtotal);
|
const partnerDiscount = partnerCodeDiscount(subtotal);
|
||||||
|
const loyaltyDiscount = treuebonusDiscount();
|
||||||
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
|
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
|
||||||
currentTotal = total;
|
currentTotal = total;
|
||||||
|
|
||||||
@@ -213,6 +215,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
|
|||||||
}).join("")}
|
}).join("")}
|
||||||
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
|
${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>` : ""}
|
${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"><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>
|
<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);
|
alert(emptyCartAlert);
|
||||||
return;
|
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;
|
location.href = thankYouPath;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
<span class="account-nav-icon" aria-hidden="true">👤</span>
|
<span class="account-nav-icon" aria-hidden="true">👤</span>
|
||||||
<span class="account-nav-label">{t.accountDash.statData}</span>
|
<span class="account-nav-label">{t.accountDash.statData}</span>
|
||||||
</button>
|
</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</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 }}}>
|
<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 };
|
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels , loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
|
// 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 wunschlisteSection = document.getElementById("wunschliste");
|
||||||
const recentSection = document.getElementById("zuletzt-angesehen");
|
const recentSection = document.getElementById("zuletzt-angesehen");
|
||||||
const datenSection = document.getElementById("meine-daten");
|
const datenSection = document.getElementById("meine-daten");
|
||||||
|
const treuebonusSection = document.getElementById("treuebonus");
|
||||||
const orderCards = Array.from(document.querySelectorAll(".order-card"));
|
const orderCards = Array.from(document.querySelectorAll(".order-card"));
|
||||||
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
|
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 (bestellungenSection) bestellungenSection.style.display = "none";
|
||||||
if (wunschlisteSection) wunschlisteSection.style.display = "none";
|
if (wunschlisteSection) wunschlisteSection.style.display = "none";
|
||||||
if (datenSection) datenSection.style.display = "none";
|
if (datenSection) datenSection.style.display = "none";
|
||||||
|
if (treuebonusSection) treuebonusSection.style.display = "none";
|
||||||
if (recentSection) recentSection.style.display = "none";
|
if (recentSection) recentSection.style.display = "none";
|
||||||
|
|
||||||
if (tab === null) {
|
if (tab === null) {
|
||||||
@@ -334,6 +360,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab === "treuebonus") {
|
||||||
|
if (treuebonusSection) treuebonusSection.style.display = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (bestellungenSection) bestellungenSection.style.display = "";
|
if (bestellungenSection) bestellungenSection.style.display = "";
|
||||||
let visibleCount = 0;
|
let visibleCount = 0;
|
||||||
orderCards.forEach((card) => {
|
orderCards.forEach((card) => {
|
||||||
@@ -359,9 +390,9 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
setActiveTab(null);
|
setActiveTab(null);
|
||||||
</script>
|
</script>
|
||||||
<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";
|
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
|
// 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
|
// 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();
|
renderProfil();
|
||||||
window.addEventListener("account:changed", 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
|
// Benutzername ändern
|
||||||
const saveBtn = document.getElementById("profile-username-save");
|
const saveBtn = document.getElementById("profile-username-save");
|
||||||
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
|
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ const t = useTranslations(lang);
|
|||||||
<a class="btn btn-primary" href="/ch/shop/">{t.common.browseShop}</a>
|
<a class="btn btn-primary" href="/ch/shop/">{t.common.browseShop}</a>
|
||||||
</div>
|
</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 id="cart-content" class="cart-layout" style="display:none;">
|
||||||
<div class="card cart-items" id="cart-items"></div>
|
<div class="card cart-items" id="cart-items"></div>
|
||||||
<aside class="card cart-summary">
|
<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"><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="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="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">
|
<div class="coupon-row">
|
||||||
<label for="coupon-input">{t.cart.couponLabel}</label>
|
<label for="coupon-input">{t.cart.couponLabel}</label>
|
||||||
<div class="coupon-input-group">
|
<div class="coupon-input-group">
|
||||||
@@ -66,18 +77,19 @@ const t = useTranslations(lang);
|
|||||||
</section>
|
</section>
|
||||||
</Layout>
|
</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.
|
// 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>
|
||||||
<script type="module">
|
<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 { formatPrice } from "../../../i18n/format";
|
||||||
import { getProduct, products } from "../../../data/products";
|
import { getProduct, products } from "../../../data/products";
|
||||||
import { berechneVersandkosten } from "../../../data/versand";
|
import { berechneVersandkosten } from "../../../data/versand";
|
||||||
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt";
|
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");
|
const landSelect = document.getElementById("cart-land");
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
@@ -101,6 +113,14 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
return { item: i, produkt, zeilenpreis, mengenProzent };
|
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 }) => {
|
itemsEl.innerHTML = zeilen.map(({ item: i, produkt, zeilenpreis, mengenProzent }) => {
|
||||||
const foto = produkt?.bilder?.[0];
|
const foto = produkt?.bilder?.[0];
|
||||||
const thumb = foto
|
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 mengenBadge = mengenProzent > 0 ? `<div class="small mengenrabatt-badge">${mengenrabattTemplate.replace("__P__", mengenProzent)}</div>` : "";
|
||||||
const ausgewaehlt = istAusgewaehlt(i);
|
const ausgewaehlt = istAusgewaehlt(i);
|
||||||
const keptBadge = !ausgewaehlt ? `<div class="small kept-in-cart-badge">🕓 ${keptInCartLabel}</div>` : "";
|
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 `
|
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}" />
|
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
|
||||||
${thumb}
|
${thumb}
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@@ -118,6 +142,7 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
|
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
|
||||||
${mengenBadge}
|
${mengenBadge}
|
||||||
${keptBadge}
|
${keptBadge}
|
||||||
|
${loyaltyControl}
|
||||||
</div>
|
</div>
|
||||||
<div class="qty-controls">
|
<div class="qty-controls">
|
||||||
<button data-action="dec" data-slug="${i.slug}" aria-label="−">−</button>
|
<button data-action="dec" data-slug="${i.slug}" aria-label="−">−</button>
|
||||||
@@ -130,6 +155,13 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
`;
|
`;
|
||||||
}).join("");
|
}).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
|
// 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.
|
// bleiben sichtbar im Warenkorb liegen (leicht abgedunkelt), ohne mitgekauft zu werden.
|
||||||
const ausgewaehlteZeilen = zeilen.filter(({ item: i }) => istAusgewaehlt(i));
|
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 : "";
|
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) => {
|
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
|
||||||
el.addEventListener("click", (e) => {
|
el.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -232,5 +275,8 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
|
|
||||||
render();
|
render();
|
||||||
window.addEventListener("cart:changed", 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);
|
landSelect.addEventListener("change", render);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -173,22 +173,23 @@ const t = useTranslations(lang);
|
|||||||
</section>
|
</section>
|
||||||
</Layout>
|
</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
|
// 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
|
// `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,
|
// module" fehlschlagen. Deshalb hier nur die vom Server gerenderten Werte auf window ablegen,
|
||||||
// die eigentliche Logik läuft im separaten <script type="module"> darunter.
|
// 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>
|
||||||
<script type="module">
|
<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 { formatPrice } from "../../i18n/format";
|
||||||
import { products } from "../../data/products";
|
import { products } from "../../data/products";
|
||||||
import { berechneVersandkosten } from "../../data/versand";
|
import { berechneVersandkosten } from "../../data/versand";
|
||||||
import { effektiverZeilenpreis } from "../../data/rabatt";
|
import { effektiverZeilenpreis } from "../../data/rabatt";
|
||||||
import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbindung";
|
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 summary = document.getElementById("checkout-summary");
|
||||||
const landSelect = document.getElementById("land");
|
const landSelect = document.getElementById("land");
|
||||||
@@ -205,6 +206,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbi
|
|||||||
const discount = couponDiscount(subtotal);
|
const discount = couponDiscount(subtotal);
|
||||||
const partnerCode = appliedPartnerCode();
|
const partnerCode = appliedPartnerCode();
|
||||||
const partnerDiscount = partnerCodeDiscount(subtotal);
|
const partnerDiscount = partnerCodeDiscount(subtotal);
|
||||||
|
const loyaltyDiscount = treuebonusDiscount();
|
||||||
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
|
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
|
||||||
currentTotal = total;
|
currentTotal = total;
|
||||||
|
|
||||||
@@ -216,6 +218,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbi
|
|||||||
}).join("")}
|
}).join("")}
|
||||||
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
|
${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>` : ""}
|
${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"><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>
|
<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);
|
alert(emptyCartAlert);
|
||||||
return;
|
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;
|
location.href = thankYouPath;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -173,19 +173,20 @@ const t = useTranslations(lang);
|
|||||||
</section>
|
</section>
|
||||||
</Layout>
|
</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.
|
// 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>
|
||||||
<script type="module">
|
<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 { formatPrice } from "../../../i18n/format";
|
||||||
import { products } from "../../../data/products";
|
import { products } from "../../../data/products";
|
||||||
import { berechneVersandkosten } from "../../../data/versand";
|
import { berechneVersandkosten } from "../../../data/versand";
|
||||||
import { effektiverZeilenpreis } from "../../../data/rabatt";
|
import { effektiverZeilenpreis } from "../../../data/rabatt";
|
||||||
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
|
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 summary = document.getElementById("checkout-summary");
|
||||||
const landSelect = document.getElementById("land");
|
const landSelect = document.getElementById("land");
|
||||||
@@ -202,6 +203,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
|
|||||||
const discount = couponDiscount(subtotal);
|
const discount = couponDiscount(subtotal);
|
||||||
const partnerCode = appliedPartnerCode();
|
const partnerCode = appliedPartnerCode();
|
||||||
const partnerDiscount = partnerCodeDiscount(subtotal);
|
const partnerDiscount = partnerCodeDiscount(subtotal);
|
||||||
|
const loyaltyDiscount = treuebonusDiscount();
|
||||||
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
|
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
|
||||||
currentTotal = total;
|
currentTotal = total;
|
||||||
|
|
||||||
@@ -213,6 +215,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
|
|||||||
}).join("")}
|
}).join("")}
|
||||||
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
|
${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>` : ""}
|
${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"><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>
|
<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);
|
alert(emptyCartAlert);
|
||||||
return;
|
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;
|
location.href = thankYouPath;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
<span class="account-nav-icon" aria-hidden="true">👤</span>
|
<span class="account-nav-icon" aria-hidden="true">👤</span>
|
||||||
<span class="account-nav-label">{t.accountDash.statData}</span>
|
<span class="account-nav-label">{t.accountDash.statData}</span>
|
||||||
</button>
|
</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</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 }}}>
|
<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 };
|
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels , loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
|
// 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 wunschlisteSection = document.getElementById("wunschliste");
|
||||||
const recentSection = document.getElementById("zuletzt-angesehen");
|
const recentSection = document.getElementById("zuletzt-angesehen");
|
||||||
const datenSection = document.getElementById("meine-daten");
|
const datenSection = document.getElementById("meine-daten");
|
||||||
|
const treuebonusSection = document.getElementById("treuebonus");
|
||||||
const orderCards = Array.from(document.querySelectorAll(".order-card"));
|
const orderCards = Array.from(document.querySelectorAll(".order-card"));
|
||||||
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
|
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 (bestellungenSection) bestellungenSection.style.display = "none";
|
||||||
if (wunschlisteSection) wunschlisteSection.style.display = "none";
|
if (wunschlisteSection) wunschlisteSection.style.display = "none";
|
||||||
if (datenSection) datenSection.style.display = "none";
|
if (datenSection) datenSection.style.display = "none";
|
||||||
|
if (treuebonusSection) treuebonusSection.style.display = "none";
|
||||||
if (recentSection) recentSection.style.display = "none";
|
if (recentSection) recentSection.style.display = "none";
|
||||||
|
|
||||||
if (tab === null) {
|
if (tab === null) {
|
||||||
@@ -334,6 +360,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab === "treuebonus") {
|
||||||
|
if (treuebonusSection) treuebonusSection.style.display = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (bestellungenSection) bestellungenSection.style.display = "";
|
if (bestellungenSection) bestellungenSection.style.display = "";
|
||||||
let visibleCount = 0;
|
let visibleCount = 0;
|
||||||
orderCards.forEach((card) => {
|
orderCards.forEach((card) => {
|
||||||
@@ -359,9 +390,9 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
setActiveTab(null);
|
setActiveTab(null);
|
||||||
</script>
|
</script>
|
||||||
<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";
|
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
|
// 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
|
// 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();
|
renderProfil();
|
||||||
window.addEventListener("account:changed", 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
|
// Benutzername ändern
|
||||||
const saveBtn = document.getElementById("profile-username-save");
|
const saveBtn = document.getElementById("profile-username-save");
|
||||||
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
|
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ const t = useTranslations(lang);
|
|||||||
<a class="btn btn-primary" href="/en/shop/">{t.common.browseShop}</a>
|
<a class="btn btn-primary" href="/en/shop/">{t.common.browseShop}</a>
|
||||||
</div>
|
</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 id="cart-content" class="cart-layout" style="display:none;">
|
||||||
<div class="card cart-items" id="cart-items"></div>
|
<div class="card cart-items" id="cart-items"></div>
|
||||||
<aside class="card cart-summary">
|
<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"><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="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="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">
|
<div class="coupon-row">
|
||||||
<label for="coupon-input">{t.cart.couponLabel}</label>
|
<label for="coupon-input">{t.cart.couponLabel}</label>
|
||||||
<div class="coupon-input-group">
|
<div class="coupon-input-group">
|
||||||
@@ -66,18 +77,19 @@ const t = useTranslations(lang);
|
|||||||
</section>
|
</section>
|
||||||
</Layout>
|
</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.
|
// 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>
|
||||||
<script type="module">
|
<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 { formatPrice } from "../../../i18n/format";
|
||||||
import { getProduct, products } from "../../../data/products";
|
import { getProduct, products } from "../../../data/products";
|
||||||
import { berechneVersandkosten } from "../../../data/versand";
|
import { berechneVersandkosten } from "../../../data/versand";
|
||||||
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt";
|
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");
|
const landSelect = document.getElementById("cart-land");
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
@@ -101,6 +113,14 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
return { item: i, produkt, zeilenpreis, mengenProzent };
|
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 }) => {
|
itemsEl.innerHTML = zeilen.map(({ item: i, produkt, zeilenpreis, mengenProzent }) => {
|
||||||
const foto = produkt?.bilder?.[0];
|
const foto = produkt?.bilder?.[0];
|
||||||
const thumb = foto
|
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 mengenBadge = mengenProzent > 0 ? `<div class="small mengenrabatt-badge">${mengenrabattTemplate.replace("__P__", mengenProzent)}</div>` : "";
|
||||||
const ausgewaehlt = istAusgewaehlt(i);
|
const ausgewaehlt = istAusgewaehlt(i);
|
||||||
const keptBadge = !ausgewaehlt ? `<div class="small kept-in-cart-badge">🕓 ${keptInCartLabel}</div>` : "";
|
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 `
|
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}" />
|
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
|
||||||
${thumb}
|
${thumb}
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@@ -118,6 +142,7 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
|
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
|
||||||
${mengenBadge}
|
${mengenBadge}
|
||||||
${keptBadge}
|
${keptBadge}
|
||||||
|
${loyaltyControl}
|
||||||
</div>
|
</div>
|
||||||
<div class="qty-controls">
|
<div class="qty-controls">
|
||||||
<button data-action="dec" data-slug="${i.slug}" aria-label="−">−</button>
|
<button data-action="dec" data-slug="${i.slug}" aria-label="−">−</button>
|
||||||
@@ -130,6 +155,13 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
`;
|
`;
|
||||||
}).join("");
|
}).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
|
// 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.
|
// bleiben sichtbar im Warenkorb liegen (leicht abgedunkelt), ohne mitgekauft zu werden.
|
||||||
const ausgewaehlteZeilen = zeilen.filter(({ item: i }) => istAusgewaehlt(i));
|
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 : "";
|
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) => {
|
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
|
||||||
el.addEventListener("click", (e) => {
|
el.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -232,5 +275,8 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
|
|
||||||
render();
|
render();
|
||||||
window.addEventListener("cart:changed", 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);
|
landSelect.addEventListener("change", render);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -173,19 +173,20 @@ const t = useTranslations(lang);
|
|||||||
</section>
|
</section>
|
||||||
</Layout>
|
</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.
|
// 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>
|
||||||
<script type="module">
|
<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 { formatPrice } from "../../../i18n/format";
|
||||||
import { products } from "../../../data/products";
|
import { products } from "../../../data/products";
|
||||||
import { berechneVersandkosten } from "../../../data/versand";
|
import { berechneVersandkosten } from "../../../data/versand";
|
||||||
import { effektiverZeilenpreis } from "../../../data/rabatt";
|
import { effektiverZeilenpreis } from "../../../data/rabatt";
|
||||||
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
|
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 summary = document.getElementById("checkout-summary");
|
||||||
const landSelect = document.getElementById("land");
|
const landSelect = document.getElementById("land");
|
||||||
@@ -202,6 +203,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
|
|||||||
const discount = couponDiscount(subtotal);
|
const discount = couponDiscount(subtotal);
|
||||||
const partnerCode = appliedPartnerCode();
|
const partnerCode = appliedPartnerCode();
|
||||||
const partnerDiscount = partnerCodeDiscount(subtotal);
|
const partnerDiscount = partnerCodeDiscount(subtotal);
|
||||||
|
const loyaltyDiscount = treuebonusDiscount();
|
||||||
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
|
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
|
||||||
currentTotal = total;
|
currentTotal = total;
|
||||||
|
|
||||||
@@ -213,6 +215,7 @@ import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankve
|
|||||||
}).join("")}
|
}).join("")}
|
||||||
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
|
${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>` : ""}
|
${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"><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>
|
<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);
|
alert(emptyCartAlert);
|
||||||
return;
|
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;
|
location.href = thankYouPath;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
<span class="account-nav-icon" aria-hidden="true">👤</span>
|
<span class="account-nav-icon" aria-hidden="true">👤</span>
|
||||||
<span class="account-nav-label">{t.accountDash.statData}</span>
|
<span class="account-nav-label">{t.accountDash.statData}</span>
|
||||||
</button>
|
</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</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 }}}>
|
<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 };
|
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels , loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
|
// 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 wunschlisteSection = document.getElementById("wunschliste");
|
||||||
const recentSection = document.getElementById("zuletzt-angesehen");
|
const recentSection = document.getElementById("zuletzt-angesehen");
|
||||||
const datenSection = document.getElementById("meine-daten");
|
const datenSection = document.getElementById("meine-daten");
|
||||||
|
const treuebonusSection = document.getElementById("treuebonus");
|
||||||
const orderCards = Array.from(document.querySelectorAll(".order-card"));
|
const orderCards = Array.from(document.querySelectorAll(".order-card"));
|
||||||
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
|
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 (bestellungenSection) bestellungenSection.style.display = "none";
|
||||||
if (wunschlisteSection) wunschlisteSection.style.display = "none";
|
if (wunschlisteSection) wunschlisteSection.style.display = "none";
|
||||||
if (datenSection) datenSection.style.display = "none";
|
if (datenSection) datenSection.style.display = "none";
|
||||||
|
if (treuebonusSection) treuebonusSection.style.display = "none";
|
||||||
if (recentSection) recentSection.style.display = "none";
|
if (recentSection) recentSection.style.display = "none";
|
||||||
|
|
||||||
if (tab === null) {
|
if (tab === null) {
|
||||||
@@ -334,6 +360,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab === "treuebonus") {
|
||||||
|
if (treuebonusSection) treuebonusSection.style.display = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (bestellungenSection) bestellungenSection.style.display = "";
|
if (bestellungenSection) bestellungenSection.style.display = "";
|
||||||
let visibleCount = 0;
|
let visibleCount = 0;
|
||||||
orderCards.forEach((card) => {
|
orderCards.forEach((card) => {
|
||||||
@@ -359,9 +390,9 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
setActiveTab(null);
|
setActiveTab(null);
|
||||||
</script>
|
</script>
|
||||||
<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";
|
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
|
// 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
|
// 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();
|
renderProfil();
|
||||||
window.addEventListener("account:changed", 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
|
// Benutzername ändern
|
||||||
const saveBtn = document.getElementById("profile-username-save");
|
const saveBtn = document.getElementById("profile-username-save");
|
||||||
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
|
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ const t = useTranslations(lang);
|
|||||||
<a class="btn btn-primary" href="/fr/shop/">{t.common.browseShop}</a>
|
<a class="btn btn-primary" href="/fr/shop/">{t.common.browseShop}</a>
|
||||||
</div>
|
</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 id="cart-content" class="cart-layout" style="display:none;">
|
||||||
<div class="card cart-items" id="cart-items"></div>
|
<div class="card cart-items" id="cart-items"></div>
|
||||||
<aside class="card cart-summary">
|
<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"><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="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="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">
|
<div class="coupon-row">
|
||||||
<label for="coupon-input">{t.cart.couponLabel}</label>
|
<label for="coupon-input">{t.cart.couponLabel}</label>
|
||||||
<div class="coupon-input-group">
|
<div class="coupon-input-group">
|
||||||
@@ -66,18 +77,19 @@ const t = useTranslations(lang);
|
|||||||
</section>
|
</section>
|
||||||
</Layout>
|
</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.
|
// 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>
|
||||||
<script type="module">
|
<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 { formatPrice } from "../../../i18n/format";
|
||||||
import { getProduct, products } from "../../../data/products";
|
import { getProduct, products } from "../../../data/products";
|
||||||
import { berechneVersandkosten } from "../../../data/versand";
|
import { berechneVersandkosten } from "../../../data/versand";
|
||||||
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt";
|
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");
|
const landSelect = document.getElementById("cart-land");
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
@@ -101,6 +113,14 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
return { item: i, produkt, zeilenpreis, mengenProzent };
|
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 }) => {
|
itemsEl.innerHTML = zeilen.map(({ item: i, produkt, zeilenpreis, mengenProzent }) => {
|
||||||
const foto = produkt?.bilder?.[0];
|
const foto = produkt?.bilder?.[0];
|
||||||
const thumb = foto
|
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 mengenBadge = mengenProzent > 0 ? `<div class="small mengenrabatt-badge">${mengenrabattTemplate.replace("__P__", mengenProzent)}</div>` : "";
|
||||||
const ausgewaehlt = istAusgewaehlt(i);
|
const ausgewaehlt = istAusgewaehlt(i);
|
||||||
const keptBadge = !ausgewaehlt ? `<div class="small kept-in-cart-badge">🕓 ${keptInCartLabel}</div>` : "";
|
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 `
|
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}" />
|
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
|
||||||
${thumb}
|
${thumb}
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@@ -118,6 +142,7 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
|
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
|
||||||
${mengenBadge}
|
${mengenBadge}
|
||||||
${keptBadge}
|
${keptBadge}
|
||||||
|
${loyaltyControl}
|
||||||
</div>
|
</div>
|
||||||
<div class="qty-controls">
|
<div class="qty-controls">
|
||||||
<button data-action="dec" data-slug="${i.slug}" aria-label="−">−</button>
|
<button data-action="dec" data-slug="${i.slug}" aria-label="−">−</button>
|
||||||
@@ -130,6 +155,13 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
`;
|
`;
|
||||||
}).join("");
|
}).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
|
// 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.
|
// bleiben sichtbar im Warenkorb liegen (leicht abgedunkelt), ohne mitgekauft zu werden.
|
||||||
const ausgewaehlteZeilen = zeilen.filter(({ item: i }) => istAusgewaehlt(i));
|
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 : "";
|
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) => {
|
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
|
||||||
el.addEventListener("click", (e) => {
|
el.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -232,5 +275,8 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
|
|
||||||
render();
|
render();
|
||||||
window.addEventListener("cart:changed", 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);
|
landSelect.addEventListener("change", render);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
<span class="account-nav-icon" aria-hidden="true">👤</span>
|
<span class="account-nav-icon" aria-hidden="true">👤</span>
|
||||||
<span class="account-nav-label">{t.accountDash.statData}</span>
|
<span class="account-nav-label">{t.accountDash.statData}</span>
|
||||||
</button>
|
</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -290,10 +296,28 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</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 }}}>
|
<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 };
|
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
|
// 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 wunschlisteSection = document.getElementById("wunschliste");
|
||||||
const recentSection = document.getElementById("zuletzt-angesehen");
|
const recentSection = document.getElementById("zuletzt-angesehen");
|
||||||
const datenSection = document.getElementById("meine-daten");
|
const datenSection = document.getElementById("meine-daten");
|
||||||
|
const treuebonusSection = document.getElementById("treuebonus");
|
||||||
const orderCards = Array.from(document.querySelectorAll(".order-card"));
|
const orderCards = Array.from(document.querySelectorAll(".order-card"));
|
||||||
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
|
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 (bestellungenSection) bestellungenSection.style.display = "none";
|
||||||
if (wunschlisteSection) wunschlisteSection.style.display = "none";
|
if (wunschlisteSection) wunschlisteSection.style.display = "none";
|
||||||
if (datenSection) datenSection.style.display = "none";
|
if (datenSection) datenSection.style.display = "none";
|
||||||
|
if (treuebonusSection) treuebonusSection.style.display = "none";
|
||||||
if (recentSection) recentSection.style.display = "none";
|
if (recentSection) recentSection.style.display = "none";
|
||||||
|
|
||||||
if (tab === null) {
|
if (tab === null) {
|
||||||
@@ -334,6 +360,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab === "treuebonus") {
|
||||||
|
if (treuebonusSection) treuebonusSection.style.display = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (bestellungenSection) bestellungenSection.style.display = "";
|
if (bestellungenSection) bestellungenSection.style.display = "";
|
||||||
let visibleCount = 0;
|
let visibleCount = 0;
|
||||||
orderCards.forEach((card) => {
|
orderCards.forEach((card) => {
|
||||||
@@ -359,9 +390,9 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
|
|||||||
setActiveTab(null);
|
setActiveTab(null);
|
||||||
</script>
|
</script>
|
||||||
<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";
|
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
|
// 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
|
// 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();
|
renderProfil();
|
||||||
window.addEventListener("account:changed", 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
|
// Benutzername ändern
|
||||||
const saveBtn = document.getElementById("profile-username-save");
|
const saveBtn = document.getElementById("profile-username-save");
|
||||||
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
|
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ const t = useTranslations(lang);
|
|||||||
<a class="btn btn-primary" href="/shop/">{t.common.browseShop}</a>
|
<a class="btn btn-primary" href="/shop/">{t.common.browseShop}</a>
|
||||||
</div>
|
</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 id="cart-content" class="cart-layout" style="display:none;">
|
||||||
<div class="card cart-items" id="cart-items"></div>
|
<div class="card cart-items" id="cart-items"></div>
|
||||||
<aside class="card cart-summary">
|
<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"><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="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="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">
|
<div class="coupon-row">
|
||||||
<label for="coupon-input">{t.cart.couponLabel}</label>
|
<label for="coupon-input">{t.cart.couponLabel}</label>
|
||||||
<div class="coupon-input-group">
|
<div class="coupon-input-group">
|
||||||
@@ -66,18 +77,19 @@ const t = useTranslations(lang);
|
|||||||
</section>
|
</section>
|
||||||
</Layout>
|
</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.
|
// 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>
|
||||||
<script type="module">
|
<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 { formatPrice } from "../../i18n/format";
|
||||||
import { getProduct, products } from "../../data/products";
|
import { getProduct, products } from "../../data/products";
|
||||||
import { berechneVersandkosten } from "../../data/versand";
|
import { berechneVersandkosten } from "../../data/versand";
|
||||||
import { effektiverZeilenpreis, mengenrabattProzent } from "../../data/rabatt";
|
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");
|
const landSelect = document.getElementById("cart-land");
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
@@ -101,6 +113,14 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../data/rabatt";
|
|||||||
return { item: i, produkt, zeilenpreis, mengenProzent };
|
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 }) => {
|
itemsEl.innerHTML = zeilen.map(({ item: i, produkt, zeilenpreis, mengenProzent }) => {
|
||||||
const foto = produkt?.bilder?.[0];
|
const foto = produkt?.bilder?.[0];
|
||||||
const thumb = foto
|
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 mengenBadge = mengenProzent > 0 ? `<div class="small mengenrabatt-badge">${mengenrabattTemplate.replace("__P__", mengenProzent)}</div>` : "";
|
||||||
const ausgewaehlt = istAusgewaehlt(i);
|
const ausgewaehlt = istAusgewaehlt(i);
|
||||||
const keptBadge = !ausgewaehlt ? `<div class="small kept-in-cart-badge">🕓 ${keptInCartLabel}</div>` : "";
|
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 `
|
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}" />
|
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
|
||||||
${thumb}
|
${thumb}
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@@ -118,6 +142,7 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../data/rabatt";
|
|||||||
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
|
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
|
||||||
${mengenBadge}
|
${mengenBadge}
|
||||||
${keptBadge}
|
${keptBadge}
|
||||||
|
${loyaltyControl}
|
||||||
</div>
|
</div>
|
||||||
<div class="qty-controls">
|
<div class="qty-controls">
|
||||||
<button data-action="dec" data-slug="${i.slug}" aria-label="−">−</button>
|
<button data-action="dec" data-slug="${i.slug}" aria-label="−">−</button>
|
||||||
@@ -130,6 +155,13 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../data/rabatt";
|
|||||||
`;
|
`;
|
||||||
}).join("");
|
}).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
|
// 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.
|
// bleiben sichtbar im Warenkorb liegen (leicht abgedunkelt), ohne mitgekauft zu werden.
|
||||||
const ausgewaehlteZeilen = zeilen.filter(({ item: i }) => istAusgewaehlt(i));
|
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 : "";
|
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) => {
|
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
|
||||||
el.addEventListener("click", (e) => {
|
el.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -232,5 +275,8 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../data/rabatt";
|
|||||||
|
|
||||||
render();
|
render();
|
||||||
window.addEventListener("cart:changed", 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);
|
landSelect.addEventListener("change", render);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+92
-1
@@ -18,12 +18,40 @@ export interface Account {
|
|||||||
photo: string;
|
photo: string;
|
||||||
/** "<bestellnummer>:<produkt-slug>"-Paare, zu denen schon eine Rezension abgegeben wurde. */
|
/** "<bestellnummer>:<produkt-slug>"-Paare, zu denen schon eine Rezension abgegeben wurde. */
|
||||||
bewerteteBestellungen: string[];
|
bewerteteBestellungen: string[];
|
||||||
|
/** Treuebonus (siehe TREUE_ZIEL weiter unten): Anzahl gültiger, vollständig abgeschlossener
|
||||||
|
* und bezahlter Bestellungen seit der letzten Einlösung — zählt NICHT weiter, sobald der
|
||||||
|
* Bonus aktiv ist (siehe treuebonusAktiv), damit "10 von 10" nicht einfach weiterwächst,
|
||||||
|
* während die Belohnung noch auf ihre Einlösung wartet. */
|
||||||
|
treueBestellungen: number;
|
||||||
|
/** true, sobald treueBestellungen das Ziel erreicht hat UND die Belohnung noch nicht
|
||||||
|
* eingelöst wurde. Wird bei Einlösung wieder auf false gesetzt, gleichzeitig setzt
|
||||||
|
* treuebonusEinloesen() treueBestellungen zurück auf 0 — eindeutig diesem Konto zugeordnet,
|
||||||
|
* da alles im Konto-Objekt selbst liegt und nie kontoübergreifend gelesen wird. */
|
||||||
|
treuebonusAktiv: boolean;
|
||||||
|
/** Slug des Artikels, den die Person im Warenkorb für die -20%-Belohnung ausgewählt hat
|
||||||
|
* (siehe scripts/cart.ts / warenkorb-Seite). Wird bei Einlösung wieder geleert. */
|
||||||
|
treuebonusArtikelSlug: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEY = "vandiy_account_v1";
|
const KEY = "vandiy_account_v1";
|
||||||
|
|
||||||
|
/** Ab dieser Anzahl gültiger, abgeschlossener Bestellungen wird die Treuebelohnung freigeschaltet. */
|
||||||
|
export const TREUE_ZIEL = 10;
|
||||||
|
/** Höhe des Treuebonus-Rabatts auf den frei gewählten Artikel. */
|
||||||
|
export const TREUE_RABATT_PROZENT = 20;
|
||||||
|
|
||||||
function leeresKonto(): Account {
|
function leeresKonto(): Account {
|
||||||
return { loggedIn: false, name: "", username: "", email: "", photo: "", bewerteteBestellungen: [] };
|
return {
|
||||||
|
loggedIn: false,
|
||||||
|
name: "",
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
photo: "",
|
||||||
|
bewerteteBestellungen: [],
|
||||||
|
treueBestellungen: 0,
|
||||||
|
treuebonusAktiv: false,
|
||||||
|
treuebonusArtikelSlug: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAccount(): Account {
|
export function getAccount(): Account {
|
||||||
@@ -100,3 +128,66 @@ export function markiereBestellungBewertet(bestellnummer: string, produktSlug: s
|
|||||||
saveAccount({ ...a, bewerteteBestellungen: [...a.bewerteteBestellungen, schluessel] });
|
saveAccount({ ...a, bewerteteBestellungen: [...a.bewerteteBestellungen, schluessel] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Treuebonus (10 abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel) ───
|
||||||
|
//
|
||||||
|
// WICHTIG: Es zählt hier bewusst nur eine echte "Bestellung abgeschlossen UND bezahlt"-Aktion
|
||||||
|
// (siehe registriereAbgeschlosseneBestellung(), wird im Checkout nach erfolgreichem Absenden
|
||||||
|
// aufgerufen — nicht z.B. schon beim Warenkorb füllen). Stornierte/nicht bezahlte Bestellungen
|
||||||
|
// rufen diese Funktion nie auf und zählen dadurch automatisch nicht mit. In Phase 1 (kein
|
||||||
|
// echtes Zahlungsbackend) gilt jede erfolgreich abgeschickte Demo-Bestellung als "bezahlt";
|
||||||
|
// TODO Phase 2: hier stattdessen auf ein echtes Zahlungs-Webhook-Ereignis reagieren.
|
||||||
|
|
||||||
|
/** Zählt eine gültige, abgeschlossene Bestellung für den Treuebonus. Zählt NICHT weiter, wenn
|
||||||
|
* gerade schon eine Belohnung auf Einlösung wartet (erst einlösen, dann läuft der nächste
|
||||||
|
* Zähler wieder bei 0 los). Schaltet die Belohnung automatisch frei, sobald TREUE_ZIEL erreicht
|
||||||
|
* ist. */
|
||||||
|
export function registriereAbgeschlosseneBestellung(): Account {
|
||||||
|
const a = getAccount();
|
||||||
|
if (a.treuebonusAktiv) return a;
|
||||||
|
const naechsterStand = Math.min(TREUE_ZIEL, a.treueBestellungen + 1);
|
||||||
|
const updated: Account = {
|
||||||
|
...a,
|
||||||
|
treueBestellungen: naechsterStand,
|
||||||
|
treuebonusAktiv: naechsterStand >= TREUE_ZIEL,
|
||||||
|
};
|
||||||
|
saveAccount(updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legt fest, auf welchen Artikel im Warenkorb der -20%-Treuebonus angewendet werden soll
|
||||||
|
* (oder hebt die Auswahl mit `null` wieder auf) — frei wählbar, siehe Pflichtenheft. */
|
||||||
|
export function setTreuebonusArtikel(slug: string | null) {
|
||||||
|
saveAccount({ ...getAccount(), treuebonusArtikelSlug: slug });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Markiert die Belohnung als eingelöst: deaktiviert sie, setzt den Zähler für den nächsten
|
||||||
|
* Durchlauf auf 0 zurück und leert die Artikelauswahl. Wird im Checkout nach erfolgreichem
|
||||||
|
* Absenden einer Bestellung aufgerufen, die den Treuebonus tatsächlich genutzt hat. */
|
||||||
|
export function treuebonusEinloesen(): Account {
|
||||||
|
const updated: Account = {
|
||||||
|
...getAccount(),
|
||||||
|
treueBestellungen: 0,
|
||||||
|
treuebonusAktiv: false,
|
||||||
|
treuebonusArtikelSlug: null,
|
||||||
|
};
|
||||||
|
saveAccount(updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreueFortschritt {
|
||||||
|
aktuelle: number;
|
||||||
|
ziel: number;
|
||||||
|
fehlende: number;
|
||||||
|
aktiv: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aufbereiteter Fortschritt fürs Anzeigen im Kundenkonto ("7 von 10", "Dir fehlen noch 3 …"). */
|
||||||
|
export function treueFortschritt(a: Account): TreueFortschritt {
|
||||||
|
return {
|
||||||
|
aktuelle: a.treueBestellungen,
|
||||||
|
ziel: TREUE_ZIEL,
|
||||||
|
fehlende: Math.max(0, TREUE_ZIEL - a.treueBestellungen),
|
||||||
|
aktiv: a.treuebonusAktiv,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
+26
-6
@@ -2,9 +2,10 @@
|
|||||||
// TODO Phase 2: durch echten Cloudflare-Worker-Warenkorb/Checkout ersetzen.
|
// TODO Phase 2: durch echten Cloudflare-Worker-Warenkorb/Checkout ersetzen.
|
||||||
|
|
||||||
import { getProduct } from "../data/products";
|
import { getProduct } from "../data/products";
|
||||||
import { effektiverZeilenpreis, istImSale } from "../data/rabatt";
|
import { effektiverPreis, effektiverZeilenpreis, istImSale } from "../data/rabatt";
|
||||||
import { findeGutschein, gutscheinRabatt, type Gutschein } from "../data/gutscheine";
|
import { findeGutschein, gutscheinRabatt, type Gutschein } from "../data/gutscheine";
|
||||||
import { findePartnercode, partnercodeRabatt, type Partnercode } from "../data/partnercodes";
|
import { findePartnercode, partnercodeRabatt, type Partnercode } from "../data/partnercodes";
|
||||||
|
import { getAccount, TREUE_RABATT_PROZENT } from "./account";
|
||||||
|
|
||||||
export interface CartItem {
|
export interface CartItem {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -186,9 +187,28 @@ export function partnerCodeDiscount(_subtotal?: number): number {
|
|||||||
return p ? partnercodeRabatt(p, partnerCodeEligibleSubtotal()) : 0;
|
return p ? partnercodeRabatt(p, partnerCodeEligibleSubtotal()) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gutschein und Partnercode können gleichzeitig eingelöst sein (zwei unabhängige Codes) — die
|
// Treuebonus: 20% auf GENAU EINEN frei gewählten Artikel (siehe scripts/account.ts für die
|
||||||
* Summe wird hier auf die Zwischensumme gedeckelt, damit der Gesamtpreis nie unter 0 € rutscht,
|
// Fortschritts-/Freischalt-Logik). Die Auswahl (treuebonusArtikelSlug) liegt im Konto, nicht im
|
||||||
* egal wie großzügig die einzelnen Rabatte sind. */
|
// Warenkorb selbst — bleibt so auch über einen geleerten/neu befüllten Warenkorb hinweg
|
||||||
export function totalDiscount(subtotal: number): number {
|
// eindeutig diesem Kundenkonto zugeordnet.
|
||||||
return Math.min(subtotal, couponDiscount(subtotal) + partnerCodeDiscount(subtotal));
|
|
||||||
|
/** Rabatt in Euro, den der aktive Treuebonus auf den gewählten Artikel gibt — 0, wenn kein
|
||||||
|
* Bonus aktiv ist, kein Artikel gewählt wurde, oder der gewählte Artikel gerade nicht (mehr)
|
||||||
|
* zum Kauf ausgewählt im Warenkorb liegt. Gilt bewusst nur für EIN Stück der Position, nicht
|
||||||
|
* mehrfach bei größerer Menge — "ein frei wählbarer Artikel", kein Mengenrabatt-Ersatz. */
|
||||||
|
export function treuebonusDiscount(): number {
|
||||||
|
const a = getAccount();
|
||||||
|
if (!a.treuebonusAktiv || !a.treuebonusArtikelSlug) return 0;
|
||||||
|
const item = getCart().find((i) => i.slug === a.treuebonusArtikelSlug);
|
||||||
|
if (!item || !istAusgewaehlt(item)) return 0;
|
||||||
|
const produkt = getProduct(item.slug);
|
||||||
|
const einzelpreis = produkt ? effektiverPreis(produkt) : item.preis;
|
||||||
|
return einzelpreis * (TREUE_RABATT_PROZENT / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gutschein, Partnercode und Treuebonus können gleichzeitig aktiv sein — die Summe wird hier
|
||||||
|
* auf die Zwischensumme gedeckelt, damit der Gesamtpreis nie unter 0 € rutscht, egal wie
|
||||||
|
* großzügig die einzelnen Rabatte zusammen sind. */
|
||||||
|
export function totalDiscount(subtotal: number): number {
|
||||||
|
return Math.min(subtotal, couponDiscount(subtotal) + partnerCodeDiscount(subtotal) + treuebonusDiscount());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1476,6 +1476,67 @@ a:focus-visible, button:focus-visible {
|
|||||||
.account-nav-open { --nav-color: var(--c-accent); --nav-delay: 0.4s; }
|
.account-nav-open { --nav-color: var(--c-accent); --nav-delay: 0.4s; }
|
||||||
.account-nav-wishlist { --nav-color: var(--c-sale); --nav-delay: 0.8s; }
|
.account-nav-wishlist { --nav-color: var(--c-sale); --nav-delay: 0.8s; }
|
||||||
.account-nav-data { --nav-color: #e3b23c; --nav-delay: 1.2s; }
|
.account-nav-data { --nav-color: #e3b23c; --nav-delay: 1.2s; }
|
||||||
|
.account-nav-loyalty { --nav-color: #6ecb8f; --nav-delay: 1.6s; }
|
||||||
|
/* Sobald der Treuebonus wirklich freigeschaltet ist (10/10), bekommt sein Knopf zusätzlich zur
|
||||||
|
normalen "Premium"-Optik noch einen kräftigeren, schneller pulsierenden Rahmen — soll auf
|
||||||
|
keinen Fall übersehen werden, wenn die Belohnung bereit ist. */
|
||||||
|
.account-nav-loyalty.is-ready { animation-duration: 1.4s, 1.1s; }
|
||||||
|
.account-nav-loyalty.is-ready::after { height: 4px; }
|
||||||
|
|
||||||
|
/* Treuebonus-Fortschrittskarte im Inhaltsbereich — eigener grüner Farbton, sonst dieselbe
|
||||||
|
"besondere" Formsprache wie die anderen Konto-Karten (siehe .profile-card). */
|
||||||
|
.loyalty-card { position: relative; overflow: hidden; display: flex; flex-direction: column; gap: 0.9rem; }
|
||||||
|
.loyalty-progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(244, 241, 247, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(244, 241, 247, 0.14);
|
||||||
|
}
|
||||||
|
.loyalty-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, #6ecb8f, #5fd3ec);
|
||||||
|
transition: width 0.5s var(--ease);
|
||||||
|
}
|
||||||
|
.loyalty-progress-text { font-weight: 700; font-size: 1.05rem; margin: 0; }
|
||||||
|
.loyalty-explainer { color: var(--c-text-muted); margin: 0.4rem 0 0; }
|
||||||
|
.loyalty-card-sparkle { position: absolute; font-size: 0.85rem; pointer-events: none; z-index: 1; }
|
||||||
|
.loyalty-card-sparkle-1 { top: 1.1rem; right: 1.3rem; animation: account-nav-sparkle-twinkle 2.7s ease-in-out infinite; }
|
||||||
|
.loyalty-card-sparkle-2 { bottom: 1.2rem; left: 1.3rem; font-size: 0.7rem; animation: account-nav-sparkle-twinkle 3.5s ease-in-out infinite 0.5s; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.loyalty-card-sparkle-1, .loyalty-card-sparkle-2 { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Warenkorb: Treuebonus-Banner + Auswahl direkt an der betroffenen Zeile. */
|
||||||
|
.loyalty-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.9rem;
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
border: 1.5px solid color-mix(in srgb, #6ecb8f 45%, transparent);
|
||||||
|
background: linear-gradient(120deg, color-mix(in srgb, #6ecb8f 20%, transparent), color-mix(in srgb, #5fd3ec 14%, transparent));
|
||||||
|
padding: 1rem 1.2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.loyalty-banner-icon { font-size: 1.6rem; flex-shrink: 0; }
|
||||||
|
.loyalty-banner strong { display: block; margin-bottom: 0.15em; }
|
||||||
|
.loyalty-banner p { margin: 0; color: var(--c-text-muted); font-size: 0.9rem; }
|
||||||
|
.loyalty-select {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4em;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--c-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.loyalty-select input { accent-color: #6ecb8f; width: auto; }
|
||||||
|
.cart-item.loyalty-selected { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, #6ecb8f 60%, transparent); border-radius: var(--radius-s); }
|
||||||
|
.loyalty-selected-badge { color: #6ecb8f; font-weight: 700; font-size: 0.82rem; white-space: nowrap; }
|
||||||
.account-nav-sparkle {
|
.account-nav-sparkle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.55rem;
|
top: 0.55rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user