Abosystem mit dauerhaften Rabatten und Teddy-Prämien

Drei Mitgliedschaftsstufen (Klein 4,99€/5%, Mittel 9,99€/10%, Groß 19,99€/20%)
mit automatischem Bestellrabatt und Teddy-Prämien nach Meilensteinen (12-Monats-
Rhythmus bzw. 6+12 bei Groß). Preise/Rabatte und alle Admin-Regeln (Kombinier-
barkeit mit anderen Rabatten, Fortsetzung bei Wiederanmeldung, Monate über
Stufen zusammenzählen, kostenloser Versand bei Prämie) sind über Decap CMS
einstellbar (src/content/abo-einstellungen.json).

Neuer "Mein Abo"-Bereich im Kundenkonto (6. Nav-Kachel) mit Plan-Vergleich,
Live-Status (Laufzeit, nächste Zahlung, Rabatt, Fortschritt bis zur nächsten
Prämie), Einlösen-Buttons für freigeschaltete Teddys, Kündigen/Wechseln sowie
einer übersetzten Benachrichtigungsliste. Dazu ein extra hervorgehobener
"Mein Abo verwalten"-Button neben Abmelden in der Kopfkarte.

Warenkorb/Checkout rechnen den Abo-Rabatt korrekt in die Summe ein und stellen
eingelöste Teddys als 0€-Gratisposition dar. Alles in 4 Sprachen (DE/EN/CH/FR).

Unterwegs zwei echte Bugs gefunden und behoben: addMonateISO() vermischte
lokale Zeit mit toISOString() (UTC) und verlor dadurch bei jeder Monats-
verlängerung reproduzierbar einen Tag; die Checkout-Positionsliste zeigte für
Gratis-Prämien fälschlich den vollen Preis statt 0€.

Wie beim Treuebonus gilt: Speicherung aktuell im localStorage des Geräts
(Phase 1) statt einer echten geräteübergreifenden Datenbank — folgt mit der
Zahlungsanbindung in Phase 2.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-03 16:54:25 +02:00
co-authored by Claude Sonnet 5
parent 0cb7d3ca65
commit 208d995941
21 changed files with 1902 additions and 91 deletions
+8 -8
View File
@@ -173,20 +173,19 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/ch/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount }}>
<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, aboDiscountLabel: t.cart.aboDiscount, gratisPraemieLabel: t.cart.gratisPraemie }}>
// 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, loyaltyDiscountLabel };
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, aboDiscountLabel, gratisPraemieLabel };
</script>
<script type="module">
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt } from "../../../scripts/cart";
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis } from "../../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel } = 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, aboDiscountLabel, gratisPraemieLabel } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
@@ -204,18 +203,19 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount();
const aboDiscountBetrag = aboDiscount(subtotal);
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total;
summary.innerHTML = `
${cart.map((i) => {
const produkt = products.find((p) => p.slug === i.slug);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
const zeilenpreis = zeilenpreisFuer(i);
return `<div class="row"><span>${i.menge}× ${i.name}${i.gratis ? ` <span class="small">(${gratisPraemieLabel})</span>` : ""}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
}).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""}
${aboDiscountBetrag > 0 ? `<div class="row"><span>${aboDiscountLabel}</span><span>-${formatPrice(aboDiscountBetrag, 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>
`;
+254 -5
View File
@@ -5,6 +5,7 @@ import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo";
const lang: Locale = "ch";
const t = useTranslations(lang);
@@ -49,6 +50,14 @@ const statusLabel = {
abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const;
// Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren
// Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß.
const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = {
klein: { name: t.accountDash.aboNameKlein, vorteil: t.accountDash.aboVorteilKlein },
mittel: { name: t.accountDash.aboNameMittel, vorteil: t.accountDash.aboVorteilMittel },
gross: { name: t.accountDash.aboNameGross, vorteil: t.accountDash.aboVorteilGross },
};
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
// in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3);
@@ -79,7 +88,13 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<p class="lead">{t.accountDash.lead}</p>
</div>
</div>
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
<div class="account-head-actions">
<button type="button" class="btn btn-abo-hero" id="dash-abo-btn">
<span class="btn-abo-hero-sparkle" aria-hidden="true">✨</span>
{t.accountDash.aboHeaderBtn}
</button>
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
</div>
</div>
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" /
@@ -115,6 +130,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<span class="account-nav-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button>
<button type="button" class="account-nav-item account-nav-abo" data-tab="abo">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🎫</span>
<span class="account-nav-value" id="abo-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statAbo}</span>
</button>
</nav>
</div>
</section>
@@ -314,10 +335,83 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
</div>
</section>
{/* Abosystem: drei Mitgliedschaftsstufen mit dauerhaftem Rabatt + Teddy-Prämien. Preise/Rabatte
kommen aus src/content/abo-einstellungen.json (admin-editierbar via Decap CMS), Status/
Fortschritt/Benachrichtigungen komplett aus dem Konto (siehe scripts/account.ts) und werden
per JS befüllt (Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="abo">
<div class="container">
<h2>{t.accountDash.aboTitle}</h2>
<p class="lead">{t.accountDash.aboIntro}</p>
<div class="abo-plans grid grid-3">
{ABO_STUFEN.map((stufe) => {
const cfg = aboStufenKonfig(stufe);
const labels = aboPlanLabels[stufe];
return (
<div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe}>
<span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span>
<h3>{labels.name}</h3>
<p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p>
<p class="small abo-plan-vorteil">{labels.vorteil}</p>
<button type="button" class="btn btn-primary btn-sm abo-plan-btn" data-abo-choose={stufe}>{t.accountDash.aboAbschliessenBtn}</button>
</div>
);
})}
</div>
<div class="card loyalty-card abo-status-card" id="abo-status-card" style="display:none;">
<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>
<h3>{t.accountDash.aboStatusTitle} <span class="badge" id="abo-active-badge"></span></h3>
<table class="specs">
<tbody>
<tr><th>{t.accountDash.aboFieldStufe}</th><td id="abo-field-stufe"></td></tr>
<tr><th>{t.accountDash.aboFieldPreis}</th><td id="abo-field-preis"></td></tr>
<tr><th>{t.accountDash.aboFieldRabatt}</th><td id="abo-field-rabatt"></td></tr>
<tr><th>{t.accountDash.aboFieldStart}</th><td id="abo-field-start"></td></tr>
<tr><th>{t.accountDash.aboFieldNaechsteZahlung}</th><td id="abo-field-naechste"></td></tr>
<tr><th>{t.accountDash.aboFieldLaufzeit}</th><td id="abo-field-laufzeit"></td></tr>
</tbody>
</table>
<p id="abo-fortschritt-klein" style="display:none;"></p>
<p id="abo-fortschritt-gross" style="display:none;"></p>
<p id="abo-praemien-erhalten"></p>
<p id="abo-gekuendigt-hinweis" class="small" style="display:none;"></p>
<div class="abo-praemien-block">
<h4>{t.accountDash.aboPraemienVerfuegbarTitle}</h4>
<p class="small" id="abo-keine-praemien">{t.accountDash.aboKeinePraemienVerfuegbar}</p>
<div class="abo-praemien-row">
<button type="button" class="btn btn-outline btn-sm" id="abo-einloesen-klein" style="display:none;" data-einloesen="klein">🧸 {t.accountDash.aboEinloesenKleinLabel} — {t.accountDash.aboEinloesenBtn}</button>
<button type="button" class="btn btn-outline btn-sm" id="abo-einloesen-gross" style="display:none;" data-einloesen="gross">🧸 {t.accountDash.aboEinloesenGrossLabel} — {t.accountDash.aboEinloesenBtn}</button>
</div>
<p class="small" id="abo-eingeloest-hinweis" style="display:none; color: var(--c-accent);">{t.accountDash.aboEingeloestHinweis}</p>
</div>
<div class="abo-actions">
<button type="button" class="btn btn-outline btn-sm" id="abo-zahlung-sim-btn">{t.accountDash.aboZahlungSimBtn}</button>
<button type="button" class="btn btn-outline btn-sm abo-cancel-btn" id="abo-kuendigen-btn">{t.accountDash.aboKuendigenBtn}</button>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboZahlungSimHinweis}</p>
</div>
<div class="card" id="abo-benachrichtigungen-card">
<h3>{t.accountDash.aboBenachrichtigungenTitle}</h3>
<ul class="abo-notif-list" id="abo-notif-list">
<li class="small" id="abo-notif-empty">{t.accountDash.aboKeineBenachrichtigungen}</li>
</ul>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboExplainer}</p>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/ch/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels , loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
<script 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, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -331,6 +425,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus");
const aboSection = document.getElementById("abo");
const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -343,6 +438,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none";
if (aboSection) aboSection.style.display = "none";
if (recentSection) recentSection.style.display = "none";
if (tab === null) {
@@ -365,6 +461,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return;
}
if (tab === "abo") {
if (aboSection) aboSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0;
orderCards.forEach((card) => {
@@ -387,12 +488,20 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); });
});
// Der "Mein Abo verwalten"-Button in der Kopfkarte springt direkt in den Abo-Bereich.
document.getElementById("dash-abo-btn")?.addEventListener("click", () => {
setActiveTab("abo");
aboSection?.scrollIntoView({ behavior: "smooth", block: "start" });
});
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt } from "../../../scripts/account";
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText } = (window as any).__accountDashVars;
import { fuegeTeddyGratisHinzu } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -466,6 +575,146 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderLoyalty();
window.addEventListener("account:changed", renderLoyalty);
// Abosystem: Pläne, Status, Fortschritt, Prämien und Benachrichtigungen live aus dem Konto
// rendern (Nav-Kachel + Planvergleich + Statuskarte + Benachrichtigungsliste).
const aboNameMap: Record<string, string> = { klein: aboLabels.nameKlein, mittel: aboLabels.nameMittel, gross: aboLabels.nameGross };
function renderAbo() {
const a = getAccount();
const status = aboStatus(a);
const navValue = document.getElementById("abo-nav-value");
if (navValue) navValue.textContent = status.stufe ? (status.aktiv ? "✓" : "⏸") : "";
document.querySelector(".account-nav-abo")?.classList.toggle("is-ready", status.aktiv);
// Planvergleichs-Karten: aktive Stufe markieren, Buttons entsprechend beschriften.
document.querySelectorAll<HTMLElement>("[data-abo-plan]").forEach((card) => {
const stufe = card.dataset.aboPlan;
const isCurrent = status.stufe === stufe && !a.abo.gekuendigt;
const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null;
if (badge) badge.style.display = isCurrent ? "" : "none";
card.classList.toggle("is-current-plan", isCurrent);
const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null;
if (btn) {
btn.disabled = isCurrent;
btn.textContent = isCurrent
? aboLabels.aktivBadge
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn);
}
});
const statusCard = document.getElementById("abo-status-card");
if (!status.stufe) {
if (statusCard) statusCard.style.display = "none";
return;
}
if (statusCard) statusCard.style.display = "";
const setText = (id: string, text: string) => { const el = document.getElementById(id); if (el) el.textContent = text; };
setText("abo-field-stufe", aboNameMap[status.stufe] ?? status.stufe);
setText("abo-field-preis", `${formatPrice(status.preis, aboLang)} ${aboLabels.preisSuffix}`);
setText("abo-field-rabatt", `${status.rabattProzent} %`);
setText("abo-field-start", a.abo.startDatum ?? "");
setText("abo-field-naechste", a.abo.gekuendigt ? "" : (a.abo.bezahltBisDatum ?? ""));
setText("abo-field-laufzeit", `${status.bezahlteMonate} ${aboLabels.monateSuffix}`);
const activeBadge = document.getElementById("abo-active-badge");
if (activeBadge) {
activeBadge.textContent = status.aktiv ? aboLabels.aktivBadge : aboLabels.nichtAktivBadge;
activeBadge.className = "badge " + (status.aktiv ? "abo-badge-active" : "abo-badge-inactive");
}
const fortschrittKlein = document.getElementById("abo-fortschritt-klein");
if (fortschrittKlein) {
if (status.naechsterKleinIn != null) {
fortschrittKlein.style.display = "block";
fortschrittKlein.textContent = aboLabels.fortschrittKleinTemplate.replace("{n}", String(status.naechsterKleinIn));
} else fortschrittKlein.style.display = "none";
}
const fortschrittGross = document.getElementById("abo-fortschritt-gross");
if (fortschrittGross) {
if (status.naechsterGrossIn != null) {
fortschrittGross.style.display = "block";
fortschrittGross.textContent = aboLabels.fortschrittGrossTemplate.replace("{n}", String(status.naechsterGrossIn));
} else fortschrittGross.style.display = "none";
}
setText("abo-praemien-erhalten", aboLabels.praemienErhaltenTemplate
.replace("{klein}", String(a.abo.kleinTeddyEingeloest))
.replace("{gross}", String(a.abo.grossTeddyEingeloest)));
const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis");
if (gekuendigtHinweis) {
if (a.abo.gekuendigt && a.abo.gekuendigtAm) {
gekuendigtHinweis.style.display = "block";
gekuendigtHinweis.textContent = aboLabels.gekuendigtHinweisTemplate.replace("{datum}", a.abo.gekuendigtAm);
} else gekuendigtHinweis.style.display = "none";
}
const hasKlein = status.kleinVerfuegbar > 0;
const hasGross = status.grossVerfuegbar > 0;
const einloesenKlein = document.getElementById("abo-einloesen-klein") as HTMLElement | null;
const einloesenGross = document.getElementById("abo-einloesen-gross") as HTMLElement | null;
const keinePraemien = document.getElementById("abo-keine-praemien") as HTMLElement | null;
if (einloesenKlein) einloesenKlein.style.display = hasKlein ? "inline-flex" : "none";
if (einloesenGross) einloesenGross.style.display = hasGross ? "inline-flex" : "none";
if (keinePraemien) keinePraemien.style.display = hasKlein || hasGross ? "none" : "block";
const kuendigenBtn = document.getElementById("abo-kuendigen-btn") as HTMLElement | null;
if (kuendigenBtn) kuendigenBtn.style.display = a.abo.gekuendigt ? "none" : "inline-flex";
const list = document.getElementById("abo-notif-list");
if (list) {
list.innerHTML = "";
if (a.benachrichtigungen.length === 0) {
const li = document.createElement("li");
li.className = "small";
li.id = "abo-notif-empty";
li.textContent = aboLabels.keineBenachrichtigungen;
list.appendChild(li);
} else {
a.benachrichtigungen.forEach((n) => {
const li = document.createElement("li");
li.className = "small abo-notif-item" + (n.gelesen ? "" : " abo-notif-unread");
let text = aboLabels.notifKeyLabels[n.key] ?? n.key;
if (n.stufe) text = text.replace("{stufe}", aboNameMap[n.stufe] ?? n.stufe);
li.textContent = `${n.datum} — ${text}`;
list.appendChild(li);
});
}
}
}
renderAbo();
window.addEventListener("account:changed", renderAbo);
document.querySelectorAll<HTMLButtonElement>("[data-abo-choose]").forEach((btn) => {
btn.addEventListener("click", () => {
const stufe = btn.dataset.aboChoose as "klein" | "mittel" | "gross" | undefined;
if (!stufe) return;
aboAbschliessen(stufe);
});
});
document.getElementById("abo-zahlung-sim-btn")?.addEventListener("click", () => { aboZahlungSimulieren(); });
document.getElementById("abo-kuendigen-btn")?.addEventListener("click", () => {
if (window.confirm(aboLabels.kuendigenConfirm)) aboKuendigen();
});
document.querySelectorAll<HTMLButtonElement>("[data-einloesen]").forEach((btn) => {
btn.addEventListener("click", () => {
const art = btn.dataset.einloesen as "klein" | "gross" | undefined;
if (!art) return;
teddyEinloesen(art);
fuegeTeddyGratisHinzu(art);
const hinweis = document.getElementById("abo-eingeloest-hinweis");
if (hinweis) {
hinweis.style.display = "block";
setTimeout(() => { hinweis.style.display = "none"; }, 2500);
}
});
});
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+21 -7
View File
@@ -40,6 +40,7 @@ const t = useTranslations(lang);
<div class="summary-row discount-row" id="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="abo-discount-row" style="display:none;"><span>{t.cart.aboDiscount}</span><span id="sum-abo-discount">-0,00 €</span></div>
<div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group">
@@ -77,19 +78,19 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltySelectLabel: t.cart.loyaltySelectLabel, loyaltySelectedLabel: t.cart.loyaltySelectedLabel }}>
<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, gratisLabel: t.cart.gratisPraemie }}>
// 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, loyaltySelectLabel, loyaltySelectedLabel };
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt, setAusgewaehlt } from "../../../scripts/cart";
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, setAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { getProduct, products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt";
import { 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, loyaltySelectLabel, loyaltySelectedLabel } = window.__cartVars;
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel } = window.__cartVars;
const landSelect = document.getElementById("cart-land");
function render() {
@@ -108,7 +109,7 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
const zeilen = cart.map((i) => {
const produkt = getProduct(i.slug);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
const zeilenpreis = zeilenpreisFuer(i);
const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
return { item: i, produkt, zeilenpreis, mengenProzent };
});
@@ -133,15 +134,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
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>`;
const gratisBadge = i.gratis ? `<div class="small loyalty-selected-badge">🧸 ${gratisLabel}</div>` : "";
return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
${thumb}
<div class="info">
<strong>${i.name}</strong>
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
${!i.gratis ? `<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>` : ""}
${mengenBadge}
${keptBadge}
${gratisBadge}
${loyaltyControl}
</div>
<div class="qty-controls">
@@ -227,6 +230,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
}
}
const aboDiscountRow = document.getElementById("abo-discount-row");
const aboDiscountBetrag = aboDiscount(subtotal);
if (aboDiscountRow) {
if (aboDiscountBetrag > 0) {
aboDiscountRow.style.display = "flex";
document.getElementById("sum-abo-discount").textContent = "-" + formatPrice(aboDiscountBetrag, cartLang);
} else {
aboDiscountRow.style.display = "none";
}
}
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
+8 -8
View File
@@ -173,23 +173,22 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount }}>
<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, aboDiscountLabel: t.cart.aboDiscount, gratisPraemieLabel: t.cart.gratisPraemie }}>
// WICHTIG: define:vars-Skripte werden von Astro in ein IIFE gepackt (kein ES-Modul) — echte
// `import`-Anweisungen würden hier zur Laufzeit mit "Cannot use import statement outside a
// module" fehlschlagen. Deshalb hier nur die vom Server gerenderten Werte auf window ablegen,
// die eigentliche Logik läuft im separaten <script type="module"> darunter.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel };
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, aboDiscountLabel, gratisPraemieLabel };
</script>
<script type="module">
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt } from "../../scripts/cart";
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../scripts/cart";
import { formatPrice } from "../../i18n/format";
import { products } from "../../data/products";
import { berechneVersandkosten } from "../../data/versand";
import { effektiverZeilenpreis } from "../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../scripts/account";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel } = 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, aboDiscountLabel, gratisPraemieLabel } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
@@ -207,18 +206,19 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount();
const aboDiscountBetrag = aboDiscount(subtotal);
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total;
summary.innerHTML = `
${cart.map((i) => {
const produkt = products.find((p) => p.slug === i.slug);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
const zeilenpreis = zeilenpreisFuer(i);
return `<div class="row"><span>${i.menge}× ${i.name}${i.gratis ? ` <span class="small">(${gratisPraemieLabel})</span>` : ""}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
}).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""}
${aboDiscountBetrag > 0 ? `<div class="row"><span>${aboDiscountLabel}</span><span>-${formatPrice(aboDiscountBetrag, 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>
`;
+8 -8
View File
@@ -173,20 +173,19 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/en/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount }}>
<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, aboDiscountLabel: t.cart.aboDiscount, gratisPraemieLabel: t.cart.gratisPraemie }}>
// 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, loyaltyDiscountLabel };
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, aboDiscountLabel, gratisPraemieLabel };
</script>
<script type="module">
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt } from "../../../scripts/cart";
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis } from "../../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel } = 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, aboDiscountLabel, gratisPraemieLabel } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
@@ -204,18 +203,19 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount();
const aboDiscountBetrag = aboDiscount(subtotal);
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total;
summary.innerHTML = `
${cart.map((i) => {
const produkt = products.find((p) => p.slug === i.slug);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
const zeilenpreis = zeilenpreisFuer(i);
return `<div class="row"><span>${i.menge}× ${i.name}${i.gratis ? ` <span class="small">(${gratisPraemieLabel})</span>` : ""}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
}).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""}
${aboDiscountBetrag > 0 ? `<div class="row"><span>${aboDiscountLabel}</span><span>-${formatPrice(aboDiscountBetrag, 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>
`;
+254 -5
View File
@@ -5,6 +5,7 @@ import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo";
const lang: Locale = "en";
const t = useTranslations(lang);
@@ -49,6 +50,14 @@ const statusLabel = {
abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const;
// Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren
// Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß.
const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = {
klein: { name: t.accountDash.aboNameKlein, vorteil: t.accountDash.aboVorteilKlein },
mittel: { name: t.accountDash.aboNameMittel, vorteil: t.accountDash.aboVorteilMittel },
gross: { name: t.accountDash.aboNameGross, vorteil: t.accountDash.aboVorteilGross },
};
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
// in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3);
@@ -79,7 +88,13 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<p class="lead">{t.accountDash.lead}</p>
</div>
</div>
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
<div class="account-head-actions">
<button type="button" class="btn btn-abo-hero" id="dash-abo-btn">
<span class="btn-abo-hero-sparkle" aria-hidden="true">✨</span>
{t.accountDash.aboHeaderBtn}
</button>
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
</div>
</div>
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" /
@@ -115,6 +130,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<span class="account-nav-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button>
<button type="button" class="account-nav-item account-nav-abo" data-tab="abo">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🎫</span>
<span class="account-nav-value" id="abo-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statAbo}</span>
</button>
</nav>
</div>
</section>
@@ -314,10 +335,83 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
</div>
</section>
{/* Abosystem: drei Mitgliedschaftsstufen mit dauerhaftem Rabatt + Teddy-Prämien. Preise/Rabatte
kommen aus src/content/abo-einstellungen.json (admin-editierbar via Decap CMS), Status/
Fortschritt/Benachrichtigungen komplett aus dem Konto (siehe scripts/account.ts) und werden
per JS befüllt (Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="abo">
<div class="container">
<h2>{t.accountDash.aboTitle}</h2>
<p class="lead">{t.accountDash.aboIntro}</p>
<div class="abo-plans grid grid-3">
{ABO_STUFEN.map((stufe) => {
const cfg = aboStufenKonfig(stufe);
const labels = aboPlanLabels[stufe];
return (
<div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe}>
<span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span>
<h3>{labels.name}</h3>
<p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p>
<p class="small abo-plan-vorteil">{labels.vorteil}</p>
<button type="button" class="btn btn-primary btn-sm abo-plan-btn" data-abo-choose={stufe}>{t.accountDash.aboAbschliessenBtn}</button>
</div>
);
})}
</div>
<div class="card loyalty-card abo-status-card" id="abo-status-card" style="display:none;">
<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>
<h3>{t.accountDash.aboStatusTitle} <span class="badge" id="abo-active-badge"></span></h3>
<table class="specs">
<tbody>
<tr><th>{t.accountDash.aboFieldStufe}</th><td id="abo-field-stufe"></td></tr>
<tr><th>{t.accountDash.aboFieldPreis}</th><td id="abo-field-preis"></td></tr>
<tr><th>{t.accountDash.aboFieldRabatt}</th><td id="abo-field-rabatt"></td></tr>
<tr><th>{t.accountDash.aboFieldStart}</th><td id="abo-field-start"></td></tr>
<tr><th>{t.accountDash.aboFieldNaechsteZahlung}</th><td id="abo-field-naechste"></td></tr>
<tr><th>{t.accountDash.aboFieldLaufzeit}</th><td id="abo-field-laufzeit"></td></tr>
</tbody>
</table>
<p id="abo-fortschritt-klein" style="display:none;"></p>
<p id="abo-fortschritt-gross" style="display:none;"></p>
<p id="abo-praemien-erhalten"></p>
<p id="abo-gekuendigt-hinweis" class="small" style="display:none;"></p>
<div class="abo-praemien-block">
<h4>{t.accountDash.aboPraemienVerfuegbarTitle}</h4>
<p class="small" id="abo-keine-praemien">{t.accountDash.aboKeinePraemienVerfuegbar}</p>
<div class="abo-praemien-row">
<button type="button" class="btn btn-outline btn-sm" id="abo-einloesen-klein" style="display:none;" data-einloesen="klein">🧸 {t.accountDash.aboEinloesenKleinLabel} — {t.accountDash.aboEinloesenBtn}</button>
<button type="button" class="btn btn-outline btn-sm" id="abo-einloesen-gross" style="display:none;" data-einloesen="gross">🧸 {t.accountDash.aboEinloesenGrossLabel} — {t.accountDash.aboEinloesenBtn}</button>
</div>
<p class="small" id="abo-eingeloest-hinweis" style="display:none; color: var(--c-accent);">{t.accountDash.aboEingeloestHinweis}</p>
</div>
<div class="abo-actions">
<button type="button" class="btn btn-outline btn-sm" id="abo-zahlung-sim-btn">{t.accountDash.aboZahlungSimBtn}</button>
<button type="button" class="btn btn-outline btn-sm abo-cancel-btn" id="abo-kuendigen-btn">{t.accountDash.aboKuendigenBtn}</button>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboZahlungSimHinweis}</p>
</div>
<div class="card" id="abo-benachrichtigungen-card">
<h3>{t.accountDash.aboBenachrichtigungenTitle}</h3>
<ul class="abo-notif-list" id="abo-notif-list">
<li class="small" id="abo-notif-empty">{t.accountDash.aboKeineBenachrichtigungen}</li>
</ul>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboExplainer}</p>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels , loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
<script 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, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -331,6 +425,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus");
const aboSection = document.getElementById("abo");
const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -343,6 +438,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none";
if (aboSection) aboSection.style.display = "none";
if (recentSection) recentSection.style.display = "none";
if (tab === null) {
@@ -365,6 +461,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return;
}
if (tab === "abo") {
if (aboSection) aboSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0;
orderCards.forEach((card) => {
@@ -387,12 +488,20 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); });
});
// Der "Mein Abo verwalten"-Button in der Kopfkarte springt direkt in den Abo-Bereich.
document.getElementById("dash-abo-btn")?.addEventListener("click", () => {
setActiveTab("abo");
aboSection?.scrollIntoView({ behavior: "smooth", block: "start" });
});
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt } from "../../../scripts/account";
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText } = (window as any).__accountDashVars;
import { fuegeTeddyGratisHinzu } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -466,6 +575,146 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderLoyalty();
window.addEventListener("account:changed", renderLoyalty);
// Abosystem: Pläne, Status, Fortschritt, Prämien und Benachrichtigungen live aus dem Konto
// rendern (Nav-Kachel + Planvergleich + Statuskarte + Benachrichtigungsliste).
const aboNameMap: Record<string, string> = { klein: aboLabels.nameKlein, mittel: aboLabels.nameMittel, gross: aboLabels.nameGross };
function renderAbo() {
const a = getAccount();
const status = aboStatus(a);
const navValue = document.getElementById("abo-nav-value");
if (navValue) navValue.textContent = status.stufe ? (status.aktiv ? "✓" : "⏸") : "";
document.querySelector(".account-nav-abo")?.classList.toggle("is-ready", status.aktiv);
// Planvergleichs-Karten: aktive Stufe markieren, Buttons entsprechend beschriften.
document.querySelectorAll<HTMLElement>("[data-abo-plan]").forEach((card) => {
const stufe = card.dataset.aboPlan;
const isCurrent = status.stufe === stufe && !a.abo.gekuendigt;
const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null;
if (badge) badge.style.display = isCurrent ? "" : "none";
card.classList.toggle("is-current-plan", isCurrent);
const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null;
if (btn) {
btn.disabled = isCurrent;
btn.textContent = isCurrent
? aboLabels.aktivBadge
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn);
}
});
const statusCard = document.getElementById("abo-status-card");
if (!status.stufe) {
if (statusCard) statusCard.style.display = "none";
return;
}
if (statusCard) statusCard.style.display = "";
const setText = (id: string, text: string) => { const el = document.getElementById(id); if (el) el.textContent = text; };
setText("abo-field-stufe", aboNameMap[status.stufe] ?? status.stufe);
setText("abo-field-preis", `${formatPrice(status.preis, aboLang)} ${aboLabels.preisSuffix}`);
setText("abo-field-rabatt", `${status.rabattProzent} %`);
setText("abo-field-start", a.abo.startDatum ?? "");
setText("abo-field-naechste", a.abo.gekuendigt ? "" : (a.abo.bezahltBisDatum ?? ""));
setText("abo-field-laufzeit", `${status.bezahlteMonate} ${aboLabels.monateSuffix}`);
const activeBadge = document.getElementById("abo-active-badge");
if (activeBadge) {
activeBadge.textContent = status.aktiv ? aboLabels.aktivBadge : aboLabels.nichtAktivBadge;
activeBadge.className = "badge " + (status.aktiv ? "abo-badge-active" : "abo-badge-inactive");
}
const fortschrittKlein = document.getElementById("abo-fortschritt-klein");
if (fortschrittKlein) {
if (status.naechsterKleinIn != null) {
fortschrittKlein.style.display = "block";
fortschrittKlein.textContent = aboLabels.fortschrittKleinTemplate.replace("{n}", String(status.naechsterKleinIn));
} else fortschrittKlein.style.display = "none";
}
const fortschrittGross = document.getElementById("abo-fortschritt-gross");
if (fortschrittGross) {
if (status.naechsterGrossIn != null) {
fortschrittGross.style.display = "block";
fortschrittGross.textContent = aboLabels.fortschrittGrossTemplate.replace("{n}", String(status.naechsterGrossIn));
} else fortschrittGross.style.display = "none";
}
setText("abo-praemien-erhalten", aboLabels.praemienErhaltenTemplate
.replace("{klein}", String(a.abo.kleinTeddyEingeloest))
.replace("{gross}", String(a.abo.grossTeddyEingeloest)));
const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis");
if (gekuendigtHinweis) {
if (a.abo.gekuendigt && a.abo.gekuendigtAm) {
gekuendigtHinweis.style.display = "block";
gekuendigtHinweis.textContent = aboLabels.gekuendigtHinweisTemplate.replace("{datum}", a.abo.gekuendigtAm);
} else gekuendigtHinweis.style.display = "none";
}
const hasKlein = status.kleinVerfuegbar > 0;
const hasGross = status.grossVerfuegbar > 0;
const einloesenKlein = document.getElementById("abo-einloesen-klein") as HTMLElement | null;
const einloesenGross = document.getElementById("abo-einloesen-gross") as HTMLElement | null;
const keinePraemien = document.getElementById("abo-keine-praemien") as HTMLElement | null;
if (einloesenKlein) einloesenKlein.style.display = hasKlein ? "inline-flex" : "none";
if (einloesenGross) einloesenGross.style.display = hasGross ? "inline-flex" : "none";
if (keinePraemien) keinePraemien.style.display = hasKlein || hasGross ? "none" : "block";
const kuendigenBtn = document.getElementById("abo-kuendigen-btn") as HTMLElement | null;
if (kuendigenBtn) kuendigenBtn.style.display = a.abo.gekuendigt ? "none" : "inline-flex";
const list = document.getElementById("abo-notif-list");
if (list) {
list.innerHTML = "";
if (a.benachrichtigungen.length === 0) {
const li = document.createElement("li");
li.className = "small";
li.id = "abo-notif-empty";
li.textContent = aboLabels.keineBenachrichtigungen;
list.appendChild(li);
} else {
a.benachrichtigungen.forEach((n) => {
const li = document.createElement("li");
li.className = "small abo-notif-item" + (n.gelesen ? "" : " abo-notif-unread");
let text = aboLabels.notifKeyLabels[n.key] ?? n.key;
if (n.stufe) text = text.replace("{stufe}", aboNameMap[n.stufe] ?? n.stufe);
li.textContent = `${n.datum} — ${text}`;
list.appendChild(li);
});
}
}
}
renderAbo();
window.addEventListener("account:changed", renderAbo);
document.querySelectorAll<HTMLButtonElement>("[data-abo-choose]").forEach((btn) => {
btn.addEventListener("click", () => {
const stufe = btn.dataset.aboChoose as "klein" | "mittel" | "gross" | undefined;
if (!stufe) return;
aboAbschliessen(stufe);
});
});
document.getElementById("abo-zahlung-sim-btn")?.addEventListener("click", () => { aboZahlungSimulieren(); });
document.getElementById("abo-kuendigen-btn")?.addEventListener("click", () => {
if (window.confirm(aboLabels.kuendigenConfirm)) aboKuendigen();
});
document.querySelectorAll<HTMLButtonElement>("[data-einloesen]").forEach((btn) => {
btn.addEventListener("click", () => {
const art = btn.dataset.einloesen as "klein" | "gross" | undefined;
if (!art) return;
teddyEinloesen(art);
fuegeTeddyGratisHinzu(art);
const hinweis = document.getElementById("abo-eingeloest-hinweis");
if (hinweis) {
hinweis.style.display = "block";
setTimeout(() => { hinweis.style.display = "none"; }, 2500);
}
});
});
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+21 -7
View File
@@ -40,6 +40,7 @@ const t = useTranslations(lang);
<div class="summary-row discount-row" id="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="abo-discount-row" style="display:none;"><span>{t.cart.aboDiscount}</span><span id="sum-abo-discount">-0,00 €</span></div>
<div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group">
@@ -77,19 +78,19 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltySelectLabel: t.cart.loyaltySelectLabel, loyaltySelectedLabel: t.cart.loyaltySelectedLabel }}>
<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, gratisLabel: t.cart.gratisPraemie }}>
// 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, loyaltySelectLabel, loyaltySelectedLabel };
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt, setAusgewaehlt } from "../../../scripts/cart";
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, setAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { getProduct, products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt";
import { 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, loyaltySelectLabel, loyaltySelectedLabel } = window.__cartVars;
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel } = window.__cartVars;
const landSelect = document.getElementById("cart-land");
function render() {
@@ -108,7 +109,7 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
const zeilen = cart.map((i) => {
const produkt = getProduct(i.slug);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
const zeilenpreis = zeilenpreisFuer(i);
const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
return { item: i, produkt, zeilenpreis, mengenProzent };
});
@@ -133,15 +134,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
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>`;
const gratisBadge = i.gratis ? `<div class="small loyalty-selected-badge">🧸 ${gratisLabel}</div>` : "";
return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
${thumb}
<div class="info">
<strong>${i.name}</strong>
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
${!i.gratis ? `<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>` : ""}
${mengenBadge}
${keptBadge}
${gratisBadge}
${loyaltyControl}
</div>
<div class="qty-controls">
@@ -227,6 +230,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
}
}
const aboDiscountRow = document.getElementById("abo-discount-row");
const aboDiscountBetrag = aboDiscount(subtotal);
if (aboDiscountRow) {
if (aboDiscountBetrag > 0) {
aboDiscountRow.style.display = "flex";
document.getElementById("sum-abo-discount").textContent = "-" + formatPrice(aboDiscountBetrag, cartLang);
} else {
aboDiscountRow.style.display = "none";
}
}
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
+8 -8
View File
@@ -173,20 +173,19 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/fr/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount }}>
<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, aboDiscountLabel: t.cart.aboDiscount, gratisPraemieLabel: t.cart.gratisPraemie }}>
// 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, loyaltyDiscountLabel };
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, aboDiscountLabel, gratisPraemieLabel };
</script>
<script type="module">
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt } from "../../../scripts/cart";
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis } from "../../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel } = 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, aboDiscountLabel, gratisPraemieLabel } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
@@ -204,18 +203,19 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount();
const aboDiscountBetrag = aboDiscount(subtotal);
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total;
summary.innerHTML = `
${cart.map((i) => {
const produkt = products.find((p) => p.slug === i.slug);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
const zeilenpreis = zeilenpreisFuer(i);
return `<div class="row"><span>${i.menge}× ${i.name}${i.gratis ? ` <span class="small">(${gratisPraemieLabel})</span>` : ""}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
}).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""}
${aboDiscountBetrag > 0 ? `<div class="row"><span>${aboDiscountLabel}</span><span>-${formatPrice(aboDiscountBetrag, 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>
`;
+254 -5
View File
@@ -5,6 +5,7 @@ import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo";
const lang: Locale = "fr";
const t = useTranslations(lang);
@@ -49,6 +50,14 @@ const statusLabel = {
abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const;
// Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren
// Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß.
const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = {
klein: { name: t.accountDash.aboNameKlein, vorteil: t.accountDash.aboVorteilKlein },
mittel: { name: t.accountDash.aboNameMittel, vorteil: t.accountDash.aboVorteilMittel },
gross: { name: t.accountDash.aboNameGross, vorteil: t.accountDash.aboVorteilGross },
};
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
// in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3);
@@ -79,7 +88,13 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<p class="lead">{t.accountDash.lead}</p>
</div>
</div>
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
<div class="account-head-actions">
<button type="button" class="btn btn-abo-hero" id="dash-abo-btn">
<span class="btn-abo-hero-sparkle" aria-hidden="true">✨</span>
{t.accountDash.aboHeaderBtn}
</button>
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
</div>
</div>
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" /
@@ -115,6 +130,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<span class="account-nav-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button>
<button type="button" class="account-nav-item account-nav-abo" data-tab="abo">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🎫</span>
<span class="account-nav-value" id="abo-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statAbo}</span>
</button>
</nav>
</div>
</section>
@@ -314,10 +335,83 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
</div>
</section>
{/* Abosystem: drei Mitgliedschaftsstufen mit dauerhaftem Rabatt + Teddy-Prämien. Preise/Rabatte
kommen aus src/content/abo-einstellungen.json (admin-editierbar via Decap CMS), Status/
Fortschritt/Benachrichtigungen komplett aus dem Konto (siehe scripts/account.ts) und werden
per JS befüllt (Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="abo">
<div class="container">
<h2>{t.accountDash.aboTitle}</h2>
<p class="lead">{t.accountDash.aboIntro}</p>
<div class="abo-plans grid grid-3">
{ABO_STUFEN.map((stufe) => {
const cfg = aboStufenKonfig(stufe);
const labels = aboPlanLabels[stufe];
return (
<div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe}>
<span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span>
<h3>{labels.name}</h3>
<p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p>
<p class="small abo-plan-vorteil">{labels.vorteil}</p>
<button type="button" class="btn btn-primary btn-sm abo-plan-btn" data-abo-choose={stufe}>{t.accountDash.aboAbschliessenBtn}</button>
</div>
);
})}
</div>
<div class="card loyalty-card abo-status-card" id="abo-status-card" style="display:none;">
<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>
<h3>{t.accountDash.aboStatusTitle} <span class="badge" id="abo-active-badge"></span></h3>
<table class="specs">
<tbody>
<tr><th>{t.accountDash.aboFieldStufe}</th><td id="abo-field-stufe"></td></tr>
<tr><th>{t.accountDash.aboFieldPreis}</th><td id="abo-field-preis"></td></tr>
<tr><th>{t.accountDash.aboFieldRabatt}</th><td id="abo-field-rabatt"></td></tr>
<tr><th>{t.accountDash.aboFieldStart}</th><td id="abo-field-start"></td></tr>
<tr><th>{t.accountDash.aboFieldNaechsteZahlung}</th><td id="abo-field-naechste"></td></tr>
<tr><th>{t.accountDash.aboFieldLaufzeit}</th><td id="abo-field-laufzeit"></td></tr>
</tbody>
</table>
<p id="abo-fortschritt-klein" style="display:none;"></p>
<p id="abo-fortschritt-gross" style="display:none;"></p>
<p id="abo-praemien-erhalten"></p>
<p id="abo-gekuendigt-hinweis" class="small" style="display:none;"></p>
<div class="abo-praemien-block">
<h4>{t.accountDash.aboPraemienVerfuegbarTitle}</h4>
<p class="small" id="abo-keine-praemien">{t.accountDash.aboKeinePraemienVerfuegbar}</p>
<div class="abo-praemien-row">
<button type="button" class="btn btn-outline btn-sm" id="abo-einloesen-klein" style="display:none;" data-einloesen="klein">🧸 {t.accountDash.aboEinloesenKleinLabel} — {t.accountDash.aboEinloesenBtn}</button>
<button type="button" class="btn btn-outline btn-sm" id="abo-einloesen-gross" style="display:none;" data-einloesen="gross">🧸 {t.accountDash.aboEinloesenGrossLabel} — {t.accountDash.aboEinloesenBtn}</button>
</div>
<p class="small" id="abo-eingeloest-hinweis" style="display:none; color: var(--c-accent);">{t.accountDash.aboEingeloestHinweis}</p>
</div>
<div class="abo-actions">
<button type="button" class="btn btn-outline btn-sm" id="abo-zahlung-sim-btn">{t.accountDash.aboZahlungSimBtn}</button>
<button type="button" class="btn btn-outline btn-sm abo-cancel-btn" id="abo-kuendigen-btn">{t.accountDash.aboKuendigenBtn}</button>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboZahlungSimHinweis}</p>
</div>
<div class="card" id="abo-benachrichtigungen-card">
<h3>{t.accountDash.aboBenachrichtigungenTitle}</h3>
<ul class="abo-notif-list" id="abo-notif-list">
<li class="small" id="abo-notif-empty">{t.accountDash.aboKeineBenachrichtigungen}</li>
</ul>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboExplainer}</p>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/fr/konto/", fallbackName: "Cliente", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText}}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels , loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
<script 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, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -331,6 +425,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus");
const aboSection = document.getElementById("abo");
const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -343,6 +438,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none";
if (aboSection) aboSection.style.display = "none";
if (recentSection) recentSection.style.display = "none";
if (tab === null) {
@@ -365,6 +461,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return;
}
if (tab === "abo") {
if (aboSection) aboSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0;
orderCards.forEach((card) => {
@@ -387,12 +488,20 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); });
});
// Der "Mein Abo verwalten"-Button in der Kopfkarte springt direkt in den Abo-Bereich.
document.getElementById("dash-abo-btn")?.addEventListener("click", () => {
setActiveTab("abo");
aboSection?.scrollIntoView({ behavior: "smooth", block: "start" });
});
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt } from "../../../scripts/account";
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText } = (window as any).__accountDashVars;
import { fuegeTeddyGratisHinzu } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -466,6 +575,146 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderLoyalty();
window.addEventListener("account:changed", renderLoyalty);
// Abosystem: Pläne, Status, Fortschritt, Prämien und Benachrichtigungen live aus dem Konto
// rendern (Nav-Kachel + Planvergleich + Statuskarte + Benachrichtigungsliste).
const aboNameMap: Record<string, string> = { klein: aboLabels.nameKlein, mittel: aboLabels.nameMittel, gross: aboLabels.nameGross };
function renderAbo() {
const a = getAccount();
const status = aboStatus(a);
const navValue = document.getElementById("abo-nav-value");
if (navValue) navValue.textContent = status.stufe ? (status.aktiv ? "✓" : "⏸") : "";
document.querySelector(".account-nav-abo")?.classList.toggle("is-ready", status.aktiv);
// Planvergleichs-Karten: aktive Stufe markieren, Buttons entsprechend beschriften.
document.querySelectorAll<HTMLElement>("[data-abo-plan]").forEach((card) => {
const stufe = card.dataset.aboPlan;
const isCurrent = status.stufe === stufe && !a.abo.gekuendigt;
const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null;
if (badge) badge.style.display = isCurrent ? "" : "none";
card.classList.toggle("is-current-plan", isCurrent);
const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null;
if (btn) {
btn.disabled = isCurrent;
btn.textContent = isCurrent
? aboLabels.aktivBadge
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn);
}
});
const statusCard = document.getElementById("abo-status-card");
if (!status.stufe) {
if (statusCard) statusCard.style.display = "none";
return;
}
if (statusCard) statusCard.style.display = "";
const setText = (id: string, text: string) => { const el = document.getElementById(id); if (el) el.textContent = text; };
setText("abo-field-stufe", aboNameMap[status.stufe] ?? status.stufe);
setText("abo-field-preis", `${formatPrice(status.preis, aboLang)} ${aboLabels.preisSuffix}`);
setText("abo-field-rabatt", `${status.rabattProzent} %`);
setText("abo-field-start", a.abo.startDatum ?? "");
setText("abo-field-naechste", a.abo.gekuendigt ? "" : (a.abo.bezahltBisDatum ?? ""));
setText("abo-field-laufzeit", `${status.bezahlteMonate} ${aboLabels.monateSuffix}`);
const activeBadge = document.getElementById("abo-active-badge");
if (activeBadge) {
activeBadge.textContent = status.aktiv ? aboLabels.aktivBadge : aboLabels.nichtAktivBadge;
activeBadge.className = "badge " + (status.aktiv ? "abo-badge-active" : "abo-badge-inactive");
}
const fortschrittKlein = document.getElementById("abo-fortschritt-klein");
if (fortschrittKlein) {
if (status.naechsterKleinIn != null) {
fortschrittKlein.style.display = "block";
fortschrittKlein.textContent = aboLabels.fortschrittKleinTemplate.replace("{n}", String(status.naechsterKleinIn));
} else fortschrittKlein.style.display = "none";
}
const fortschrittGross = document.getElementById("abo-fortschritt-gross");
if (fortschrittGross) {
if (status.naechsterGrossIn != null) {
fortschrittGross.style.display = "block";
fortschrittGross.textContent = aboLabels.fortschrittGrossTemplate.replace("{n}", String(status.naechsterGrossIn));
} else fortschrittGross.style.display = "none";
}
setText("abo-praemien-erhalten", aboLabels.praemienErhaltenTemplate
.replace("{klein}", String(a.abo.kleinTeddyEingeloest))
.replace("{gross}", String(a.abo.grossTeddyEingeloest)));
const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis");
if (gekuendigtHinweis) {
if (a.abo.gekuendigt && a.abo.gekuendigtAm) {
gekuendigtHinweis.style.display = "block";
gekuendigtHinweis.textContent = aboLabels.gekuendigtHinweisTemplate.replace("{datum}", a.abo.gekuendigtAm);
} else gekuendigtHinweis.style.display = "none";
}
const hasKlein = status.kleinVerfuegbar > 0;
const hasGross = status.grossVerfuegbar > 0;
const einloesenKlein = document.getElementById("abo-einloesen-klein") as HTMLElement | null;
const einloesenGross = document.getElementById("abo-einloesen-gross") as HTMLElement | null;
const keinePraemien = document.getElementById("abo-keine-praemien") as HTMLElement | null;
if (einloesenKlein) einloesenKlein.style.display = hasKlein ? "inline-flex" : "none";
if (einloesenGross) einloesenGross.style.display = hasGross ? "inline-flex" : "none";
if (keinePraemien) keinePraemien.style.display = hasKlein || hasGross ? "none" : "block";
const kuendigenBtn = document.getElementById("abo-kuendigen-btn") as HTMLElement | null;
if (kuendigenBtn) kuendigenBtn.style.display = a.abo.gekuendigt ? "none" : "inline-flex";
const list = document.getElementById("abo-notif-list");
if (list) {
list.innerHTML = "";
if (a.benachrichtigungen.length === 0) {
const li = document.createElement("li");
li.className = "small";
li.id = "abo-notif-empty";
li.textContent = aboLabels.keineBenachrichtigungen;
list.appendChild(li);
} else {
a.benachrichtigungen.forEach((n) => {
const li = document.createElement("li");
li.className = "small abo-notif-item" + (n.gelesen ? "" : " abo-notif-unread");
let text = aboLabels.notifKeyLabels[n.key] ?? n.key;
if (n.stufe) text = text.replace("{stufe}", aboNameMap[n.stufe] ?? n.stufe);
li.textContent = `${n.datum} — ${text}`;
list.appendChild(li);
});
}
}
}
renderAbo();
window.addEventListener("account:changed", renderAbo);
document.querySelectorAll<HTMLButtonElement>("[data-abo-choose]").forEach((btn) => {
btn.addEventListener("click", () => {
const stufe = btn.dataset.aboChoose as "klein" | "mittel" | "gross" | undefined;
if (!stufe) return;
aboAbschliessen(stufe);
});
});
document.getElementById("abo-zahlung-sim-btn")?.addEventListener("click", () => { aboZahlungSimulieren(); });
document.getElementById("abo-kuendigen-btn")?.addEventListener("click", () => {
if (window.confirm(aboLabels.kuendigenConfirm)) aboKuendigen();
});
document.querySelectorAll<HTMLButtonElement>("[data-einloesen]").forEach((btn) => {
btn.addEventListener("click", () => {
const art = btn.dataset.einloesen as "klein" | "gross" | undefined;
if (!art) return;
teddyEinloesen(art);
fuegeTeddyGratisHinzu(art);
const hinweis = document.getElementById("abo-eingeloest-hinweis");
if (hinweis) {
hinweis.style.display = "block";
setTimeout(() => { hinweis.style.display = "none"; }, 2500);
}
});
});
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+21 -7
View File
@@ -40,6 +40,7 @@ const t = useTranslations(lang);
<div class="summary-row discount-row" id="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="abo-discount-row" style="display:none;"><span>{t.cart.aboDiscount}</span><span id="sum-abo-discount">-0,00 €</span></div>
<div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group">
@@ -77,19 +78,19 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltySelectLabel: t.cart.loyaltySelectLabel, loyaltySelectedLabel: t.cart.loyaltySelectedLabel }}>
<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, gratisLabel: t.cart.gratisPraemie }}>
// 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, loyaltySelectLabel, loyaltySelectedLabel };
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt, setAusgewaehlt } from "../../../scripts/cart";
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, setAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { getProduct, products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt";
import { 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, loyaltySelectLabel, loyaltySelectedLabel } = window.__cartVars;
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel } = window.__cartVars;
const landSelect = document.getElementById("cart-land");
function render() {
@@ -108,7 +109,7 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
const zeilen = cart.map((i) => {
const produkt = getProduct(i.slug);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
const zeilenpreis = zeilenpreisFuer(i);
const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
return { item: i, produkt, zeilenpreis, mengenProzent };
});
@@ -133,15 +134,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
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>`;
const gratisBadge = i.gratis ? `<div class="small loyalty-selected-badge">🧸 ${gratisLabel}</div>` : "";
return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
${thumb}
<div class="info">
<strong>${i.name}</strong>
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
${!i.gratis ? `<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>` : ""}
${mengenBadge}
${keptBadge}
${gratisBadge}
${loyaltyControl}
</div>
<div class="qty-controls">
@@ -227,6 +230,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
}
}
const aboDiscountRow = document.getElementById("abo-discount-row");
const aboDiscountBetrag = aboDiscount(subtotal);
if (aboDiscountRow) {
if (aboDiscountBetrag > 0) {
aboDiscountRow.style.display = "flex";
document.getElementById("sum-abo-discount").textContent = "-" + formatPrice(aboDiscountBetrag, cartLang);
} else {
aboDiscountRow.style.display = "none";
}
}
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
+254 -5
View File
@@ -5,6 +5,7 @@ import { products, getProduct } from "../../data/products";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
import { formatPrice } from "../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../data/abo";
const lang: Locale = "de";
const t = useTranslations(lang);
@@ -49,6 +50,14 @@ const statusLabel = {
abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const;
// Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren
// Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß.
const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = {
klein: { name: t.accountDash.aboNameKlein, vorteil: t.accountDash.aboVorteilKlein },
mittel: { name: t.accountDash.aboNameMittel, vorteil: t.accountDash.aboVorteilMittel },
gross: { name: t.accountDash.aboNameGross, vorteil: t.accountDash.aboVorteilGross },
};
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
// in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3);
@@ -79,7 +88,13 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<p class="lead">{t.accountDash.lead}</p>
</div>
</div>
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
<div class="account-head-actions">
<button type="button" class="btn btn-abo-hero" id="dash-abo-btn">
<span class="btn-abo-hero-sparkle" aria-hidden="true">✨</span>
{t.accountDash.aboHeaderBtn}
</button>
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
</div>
</div>
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" /
@@ -115,6 +130,12 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<span class="account-nav-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button>
<button type="button" class="account-nav-item account-nav-abo" data-tab="abo">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🎫</span>
<span class="account-nav-value" id="abo-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statAbo}</span>
</button>
</nav>
</div>
</section>
@@ -314,10 +335,83 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
</div>
</section>
{/* Abosystem: drei Mitgliedschaftsstufen mit dauerhaftem Rabatt + Teddy-Prämien. Preise/Rabatte
kommen aus src/content/abo-einstellungen.json (admin-editierbar via Decap CMS), Status/
Fortschritt/Benachrichtigungen komplett aus dem Konto (siehe scripts/account.ts) und werden
per JS befüllt (Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="abo">
<div class="container">
<h2>{t.accountDash.aboTitle}</h2>
<p class="lead">{t.accountDash.aboIntro}</p>
<div class="abo-plans grid grid-3">
{ABO_STUFEN.map((stufe) => {
const cfg = aboStufenKonfig(stufe);
const labels = aboPlanLabels[stufe];
return (
<div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe}>
<span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span>
<h3>{labels.name}</h3>
<p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p>
<p class="small abo-plan-vorteil">{labels.vorteil}</p>
<button type="button" class="btn btn-primary btn-sm abo-plan-btn" data-abo-choose={stufe}>{t.accountDash.aboAbschliessenBtn}</button>
</div>
);
})}
</div>
<div class="card loyalty-card abo-status-card" id="abo-status-card" style="display:none;">
<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>
<h3>{t.accountDash.aboStatusTitle} <span class="badge" id="abo-active-badge"></span></h3>
<table class="specs">
<tbody>
<tr><th>{t.accountDash.aboFieldStufe}</th><td id="abo-field-stufe"></td></tr>
<tr><th>{t.accountDash.aboFieldPreis}</th><td id="abo-field-preis"></td></tr>
<tr><th>{t.accountDash.aboFieldRabatt}</th><td id="abo-field-rabatt"></td></tr>
<tr><th>{t.accountDash.aboFieldStart}</th><td id="abo-field-start"></td></tr>
<tr><th>{t.accountDash.aboFieldNaechsteZahlung}</th><td id="abo-field-naechste"></td></tr>
<tr><th>{t.accountDash.aboFieldLaufzeit}</th><td id="abo-field-laufzeit"></td></tr>
</tbody>
</table>
<p id="abo-fortschritt-klein" style="display:none;"></p>
<p id="abo-fortschritt-gross" style="display:none;"></p>
<p id="abo-praemien-erhalten"></p>
<p id="abo-gekuendigt-hinweis" class="small" style="display:none;"></p>
<div class="abo-praemien-block">
<h4>{t.accountDash.aboPraemienVerfuegbarTitle}</h4>
<p class="small" id="abo-keine-praemien">{t.accountDash.aboKeinePraemienVerfuegbar}</p>
<div class="abo-praemien-row">
<button type="button" class="btn btn-outline btn-sm" id="abo-einloesen-klein" style="display:none;" data-einloesen="klein">🧸 {t.accountDash.aboEinloesenKleinLabel} — {t.accountDash.aboEinloesenBtn}</button>
<button type="button" class="btn btn-outline btn-sm" id="abo-einloesen-gross" style="display:none;" data-einloesen="gross">🧸 {t.accountDash.aboEinloesenGrossLabel} — {t.accountDash.aboEinloesenBtn}</button>
</div>
<p class="small" id="abo-eingeloest-hinweis" style="display:none; color: var(--c-accent);">{t.accountDash.aboEingeloestHinweis}</p>
</div>
<div class="abo-actions">
<button type="button" class="btn btn-outline btn-sm" id="abo-zahlung-sim-btn">{t.accountDash.aboZahlungSimBtn}</button>
<button type="button" class="btn btn-outline btn-sm abo-cancel-btn" id="abo-kuendigen-btn">{t.accountDash.aboKuendigenBtn}</button>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboZahlungSimHinweis}</p>
</div>
<div class="card" id="abo-benachrichtigungen-card">
<h3>{t.accountDash.aboBenachrichtigungenTitle}</h3>
<ul class="abo-notif-list" id="abo-notif-list">
<li class="small" id="abo-notif-empty">{t.accountDash.aboKeineBenachrichtigungen}</li>
</ul>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboExplainer}</p>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/konto/", fallbackName: "Kundin", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText };
<script 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, aboLang: lang, aboLabels: { nameKlein: t.accountDash.aboNameKlein, nameMittel: t.accountDash.aboNameMittel, nameGross: t.accountDash.aboNameGross, preisSuffix: t.accountDash.aboPreisSuffix, monateSuffix: t.accountDash.aboMonateSuffix, fortschrittKleinTemplate: t.accountDash.aboFortschrittKleinTemplate, fortschrittGrossTemplate: t.accountDash.aboFortschrittGrossTemplate, praemienErhaltenTemplate: t.accountDash.aboPraemienErhaltenTemplate, aktivBadge: t.accountDash.aboAktivesLabel, nichtAktivBadge: t.accountDash.aboNichtAktivBadge, gekuendigtHinweisTemplate: t.accountDash.aboGekuendigtHinweisTemplate, kuendigenConfirm: t.accountDash.aboKuendigenConfirm, abschliessenBtn: t.accountDash.aboAbschliessenBtn, wechselnBtn: t.accountDash.aboWechselnBtn, keineBenachrichtigungen: t.accountDash.aboKeineBenachrichtigungen, notifKeyLabels: { abo_abgeschlossen: t.accountDash.notifAboAbgeschlossen, abo_gewechselt: t.accountDash.notifAboGewechselt, abo_zahlung_erfolgreich: t.accountDash.notifAboZahlungErfolgreich, abo_teddy_klein_frei: t.accountDash.notifAboTeddyKleinFrei, abo_teddy_gross_frei: t.accountDash.notifAboTeddyGrossFrei, abo_bald_praemie: t.accountDash.notifAboBaldPraemie, abo_gekuendigt: t.accountDash.notifAboGekuendigt, abo_teddy_klein_eingeloest: t.accountDash.notifAboTeddyKleinEingeloest, abo_teddy_gross_eingeloest: t.accountDash.notifAboTeddyGrossEingeloest } } }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -331,6 +425,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus");
const aboSection = document.getElementById("abo");
const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -343,6 +438,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none";
if (aboSection) aboSection.style.display = "none";
if (recentSection) recentSection.style.display = "none";
if (tab === null) {
@@ -365,6 +461,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return;
}
if (tab === "abo") {
if (aboSection) aboSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0;
orderCards.forEach((card) => {
@@ -387,12 +488,20 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); });
});
// Der "Mein Abo verwalten"-Button in der Kopfkarte springt direkt in den Abo-Bereich.
document.getElementById("dash-abo-btn")?.addEventListener("click", () => {
setActiveTab("abo");
aboSection?.scrollIntoView({ behavior: "smooth", block: "start" });
});
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt } from "../../scripts/account";
import { getAccount, isLoggedIn, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../scripts/invoice-pdf";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText } = (window as any).__accountDashVars;
import { fuegeTeddyGratisHinzu } from "../../scripts/cart";
import { formatPrice } from "../../i18n/format";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels } = (window as any).__accountDashVars;
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -466,6 +575,146 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderLoyalty();
window.addEventListener("account:changed", renderLoyalty);
// Abosystem: Pläne, Status, Fortschritt, Prämien und Benachrichtigungen live aus dem Konto
// rendern (Nav-Kachel + Planvergleich + Statuskarte + Benachrichtigungsliste).
const aboNameMap: Record<string, string> = { klein: aboLabels.nameKlein, mittel: aboLabels.nameMittel, gross: aboLabels.nameGross };
function renderAbo() {
const a = getAccount();
const status = aboStatus(a);
const navValue = document.getElementById("abo-nav-value");
if (navValue) navValue.textContent = status.stufe ? (status.aktiv ? "✓" : "⏸") : "";
document.querySelector(".account-nav-abo")?.classList.toggle("is-ready", status.aktiv);
// Planvergleichs-Karten: aktive Stufe markieren, Buttons entsprechend beschriften.
document.querySelectorAll<HTMLElement>("[data-abo-plan]").forEach((card) => {
const stufe = card.dataset.aboPlan;
const isCurrent = status.stufe === stufe && !a.abo.gekuendigt;
const badge = card.querySelector("[data-abo-active-badge]") as HTMLElement | null;
if (badge) badge.style.display = isCurrent ? "" : "none";
card.classList.toggle("is-current-plan", isCurrent);
const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null;
if (btn) {
btn.disabled = isCurrent;
btn.textContent = isCurrent
? aboLabels.aktivBadge
: (status.stufe && status.aktiv && !a.abo.gekuendigt ? aboLabels.wechselnBtn : aboLabels.abschliessenBtn);
}
});
const statusCard = document.getElementById("abo-status-card");
if (!status.stufe) {
if (statusCard) statusCard.style.display = "none";
return;
}
if (statusCard) statusCard.style.display = "";
const setText = (id: string, text: string) => { const el = document.getElementById(id); if (el) el.textContent = text; };
setText("abo-field-stufe", aboNameMap[status.stufe] ?? status.stufe);
setText("abo-field-preis", `${formatPrice(status.preis, aboLang)} ${aboLabels.preisSuffix}`);
setText("abo-field-rabatt", `${status.rabattProzent} %`);
setText("abo-field-start", a.abo.startDatum ?? "");
setText("abo-field-naechste", a.abo.gekuendigt ? "" : (a.abo.bezahltBisDatum ?? ""));
setText("abo-field-laufzeit", `${status.bezahlteMonate} ${aboLabels.monateSuffix}`);
const activeBadge = document.getElementById("abo-active-badge");
if (activeBadge) {
activeBadge.textContent = status.aktiv ? aboLabels.aktivBadge : aboLabels.nichtAktivBadge;
activeBadge.className = "badge " + (status.aktiv ? "abo-badge-active" : "abo-badge-inactive");
}
const fortschrittKlein = document.getElementById("abo-fortschritt-klein");
if (fortschrittKlein) {
if (status.naechsterKleinIn != null) {
fortschrittKlein.style.display = "block";
fortschrittKlein.textContent = aboLabels.fortschrittKleinTemplate.replace("{n}", String(status.naechsterKleinIn));
} else fortschrittKlein.style.display = "none";
}
const fortschrittGross = document.getElementById("abo-fortschritt-gross");
if (fortschrittGross) {
if (status.naechsterGrossIn != null) {
fortschrittGross.style.display = "block";
fortschrittGross.textContent = aboLabels.fortschrittGrossTemplate.replace("{n}", String(status.naechsterGrossIn));
} else fortschrittGross.style.display = "none";
}
setText("abo-praemien-erhalten", aboLabels.praemienErhaltenTemplate
.replace("{klein}", String(a.abo.kleinTeddyEingeloest))
.replace("{gross}", String(a.abo.grossTeddyEingeloest)));
const gekuendigtHinweis = document.getElementById("abo-gekuendigt-hinweis");
if (gekuendigtHinweis) {
if (a.abo.gekuendigt && a.abo.gekuendigtAm) {
gekuendigtHinweis.style.display = "block";
gekuendigtHinweis.textContent = aboLabels.gekuendigtHinweisTemplate.replace("{datum}", a.abo.gekuendigtAm);
} else gekuendigtHinweis.style.display = "none";
}
const hasKlein = status.kleinVerfuegbar > 0;
const hasGross = status.grossVerfuegbar > 0;
const einloesenKlein = document.getElementById("abo-einloesen-klein") as HTMLElement | null;
const einloesenGross = document.getElementById("abo-einloesen-gross") as HTMLElement | null;
const keinePraemien = document.getElementById("abo-keine-praemien") as HTMLElement | null;
if (einloesenKlein) einloesenKlein.style.display = hasKlein ? "inline-flex" : "none";
if (einloesenGross) einloesenGross.style.display = hasGross ? "inline-flex" : "none";
if (keinePraemien) keinePraemien.style.display = hasKlein || hasGross ? "none" : "block";
const kuendigenBtn = document.getElementById("abo-kuendigen-btn") as HTMLElement | null;
if (kuendigenBtn) kuendigenBtn.style.display = a.abo.gekuendigt ? "none" : "inline-flex";
const list = document.getElementById("abo-notif-list");
if (list) {
list.innerHTML = "";
if (a.benachrichtigungen.length === 0) {
const li = document.createElement("li");
li.className = "small";
li.id = "abo-notif-empty";
li.textContent = aboLabels.keineBenachrichtigungen;
list.appendChild(li);
} else {
a.benachrichtigungen.forEach((n) => {
const li = document.createElement("li");
li.className = "small abo-notif-item" + (n.gelesen ? "" : " abo-notif-unread");
let text = aboLabels.notifKeyLabels[n.key] ?? n.key;
if (n.stufe) text = text.replace("{stufe}", aboNameMap[n.stufe] ?? n.stufe);
li.textContent = `${n.datum} — ${text}`;
list.appendChild(li);
});
}
}
}
renderAbo();
window.addEventListener("account:changed", renderAbo);
document.querySelectorAll<HTMLButtonElement>("[data-abo-choose]").forEach((btn) => {
btn.addEventListener("click", () => {
const stufe = btn.dataset.aboChoose as "klein" | "mittel" | "gross" | undefined;
if (!stufe) return;
aboAbschliessen(stufe);
});
});
document.getElementById("abo-zahlung-sim-btn")?.addEventListener("click", () => { aboZahlungSimulieren(); });
document.getElementById("abo-kuendigen-btn")?.addEventListener("click", () => {
if (window.confirm(aboLabels.kuendigenConfirm)) aboKuendigen();
});
document.querySelectorAll<HTMLButtonElement>("[data-einloesen]").forEach((btn) => {
btn.addEventListener("click", () => {
const art = btn.dataset.einloesen as "klein" | "gross" | undefined;
if (!art) return;
teddyEinloesen(art);
fuegeTeddyGratisHinzu(art);
const hinweis = document.getElementById("abo-eingeloest-hinweis");
if (hinweis) {
hinweis.style.display = "block";
setTimeout(() => { hinweis.style.display = "none"; }, 2500);
}
});
});
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+21 -7
View File
@@ -40,6 +40,7 @@ const t = useTranslations(lang);
<div class="summary-row discount-row" id="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="abo-discount-row" style="display:none;"><span>{t.cart.aboDiscount}</span><span id="sum-abo-discount">-0,00 €</span></div>
<div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group">
@@ -77,19 +78,19 @@ const t = useTranslations(lang);
</section>
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltySelectLabel: t.cart.loyaltySelectLabel, loyaltySelectedLabel: t.cart.loyaltySelectedLabel }}>
<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, gratisLabel: t.cart.gratisPraemie }}>
// 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, loyaltySelectLabel, loyaltySelectedLabel };
window.__cartVars = { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel };
</script>
<script type="module">
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, istAusgewaehlt, setAusgewaehlt } from "../../scripts/cart";
import { getCart, updateQuantity, removeFromCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, setAusgewaehlt, zeilenpreisFuer } from "../../scripts/cart";
import { formatPrice } from "../../i18n/format";
import { getProduct, products } from "../../data/products";
import { berechneVersandkosten } from "../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../data/rabatt";
import { 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, loyaltySelectLabel, loyaltySelectedLabel } = window.__cartVars;
const { freeShipFrom, removeLabel, perItemLabel, remainingTemplate, freeShippingText, freeLabel, cartLang, mengenrabattTemplate, couponInvalid, couponAppliedTemplate, couponRemoveLabel, buyNowLabel, keptInCartLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltySelectLabel, loyaltySelectedLabel, gratisLabel } = window.__cartVars;
const landSelect = document.getElementById("cart-land");
function render() {
@@ -108,7 +109,7 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../script
const zeilen = cart.map((i) => {
const produkt = getProduct(i.slug);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis;
const zeilenpreis = zeilenpreisFuer(i);
const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
return { item: i, produkt, zeilenpreis, mengenProzent };
});
@@ -133,15 +134,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../script
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>`;
const gratisBadge = i.gratis ? `<div class="small loyalty-selected-badge">🧸 ${gratisLabel}</div>` : "";
return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
${thumb}
<div class="info">
<strong>${i.name}</strong>
<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>
${!i.gratis ? `<div class="small">${formatPrice(zeilenpreis / i.menge, cartLang)} ${perItemLabel}</div>` : ""}
${mengenBadge}
${keptBadge}
${gratisBadge}
${loyaltyControl}
</div>
<div class="qty-controls">
@@ -227,6 +230,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../script
}
}
const aboDiscountRow = document.getElementById("abo-discount-row");
const aboDiscountBetrag = aboDiscount(subtotal);
if (aboDiscountRow) {
if (aboDiscountBetrag > 0) {
aboDiscountRow.style.display = "flex";
document.getElementById("sum-abo-discount").textContent = "-" + formatPrice(aboDiscountBetrag, cartLang);
} else {
aboDiscountRow.style.display = "none";
}
}
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();