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
+54
View File
@@ -348,3 +348,57 @@ collections:
- { label: IBAN, name: iban, widget: string, required: false, hint: "z.B. DE12 3456 7890 1234 5678 90" } - { label: IBAN, name: iban, widget: string, required: false, hint: "z.B. DE12 3456 7890 1234 5678 90" }
- { label: BIC, name: bic, widget: string, required: false } - { label: BIC, name: bic, widget: string, required: false }
- { label: Bank (Name), name: bank, widget: string, required: false } - { label: Bank (Name), name: bank, widget: string, required: false }
- name: abo-einstellungen
label: "🎁 Abo-System"
icon: card_membership
files:
- name: einstellungen
label: "Abo-Stufen, Rabatte & Regeln"
file: src/content/abo-einstellungen.json
description: >
Preise, Rabatthöhen und Verhaltensregeln für das monatliche Abo-System (siehe
scripts/account.ts + data/abo.ts). Änderungen gelten ab dem nächsten Deploy für alle
Kund:innen. Die eigentlichen Kundendaten (wer welches Abo hat, Zahlungsstand,
freigeschaltete Prämien) liegen NICHT hier, sondern pro Kundenkonto (Phase 1: im
Browser der jeweiligen Person, Phase 2: in einer echten Datenbank) — hier werden nur
die grundsätzlichen Konditionen festgelegt, zu denen JEDES Abo läuft.
fields:
- label: "Kleines Abo"
name: klein
widget: object
fields:
- { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 }
- { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 }
- label: "Mittleres Abo"
name: mittel
widget: object
fields:
- { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 }
- { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 }
- label: "Großes Abo"
name: gross
widget: object
fields:
- { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 }
- { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 }
- label: "Abo-Rabatt mit anderen Rabatten kombinierbar (Gutschein/Partnercode/Treuebonus)?"
name: kombinierbarMitAnderenRabatten
widget: boolean
default: false
hint: "Aus: Bei aktivem Abo gilt NUR der Abo-Rabatt, andere Rabatte werden für diese Bestellung ignoriert. An: alle Rabatte werden zusammengezählt."
- label: "Versand bei Teddy-Prämie ebenfalls kostenlos?"
name: versandKostenlosBeiPraemie
widget: boolean
default: false
hint: "Aus: Für den kostenlosen Teddy gelten die normalen Versandregeln des Shops."
- label: "Bei Wiederanmeldung nach vollständigem Auslaufen: bisherige Laufzeit fortsetzen?"
name: fortsetzungBeiWiederanmeldung
widget: boolean
default: false
hint: "Aus: Der Monatszähler für die nächste Prämie beginnt bei einer Wiederanmeldung wieder bei 0. An: er läuft dort weiter, wo er beim Auslaufen stand. Bereits eingelöste/freigeschaltete Teddys gehen NIE verloren, unabhängig von dieser Einstellung."
- label: "Monate aus verschiedenen Abo-Stufen beim Wechsel zusammenzählen?"
name: monateUeberStufenZusammenzaehlen
widget: boolean
default: false
hint: "Aus (empfohlen): Beim Wechsel in eine andere Stufe beginnt der Monatszähler für die nächste Prämie bei 0. An: die bisher gesammelten Monate werden in die neue Stufe übernommen."
+9
View File
@@ -0,0 +1,9 @@
{
"klein": { "preis": 4.99, "rabattProzent": 5 },
"mittel": { "preis": 9.99, "rabattProzent": 10 },
"gross": { "preis": 19.99, "rabattProzent": 20 },
"kombinierbarMitAnderenRabatten": false,
"versandKostenlosBeiPraemie": false,
"fortsetzungBeiWiederanmeldung": false,
"monateUeberStufenZusammenzaehlen": false
}
+26
View File
@@ -0,0 +1,26 @@
{
"slug": "grosser-teddy",
"name": {
"de": "Großer Teddy",
"en": "Large Teddy",
"ch": "Grosse Teddy",
"fr": "Grand ours en peluche"
},
"artikelnummer": "PRAEMIE-TEDDY-GROSS",
"kategorie": "diy-plushies",
"preis": 24.9,
"bestand": 999,
"beschreibung": {
"de": "Unser handgemachter großer Teddy — als Dankeschön-Prämie im Abo-System kostenlos einlösbar, sobald du den passenden Meilenstein erreicht hast.",
"en": "Our handmade large teddy — redeemable for free as a subscription reward once you reach the matching milestone.",
"ch": "Öise vo Hang gmachti grossi Teddy — als Dankeschön-Prämie im Abo-System gratis ilösbar, sobald du dr passendi Meilestei erreicht hesch.",
"fr": "Notre grand ours en peluche fait main — à échanger gratuitement comme récompense d'abonnement une fois le palier atteint."
},
"material": "Baumwollstoff, Füllwatte OEKO-TEX",
"groesse": "ca. 30 cm",
"lieferzeit": "24 Werktage",
"badges": [
"handgemacht"
],
"bilder": []
}
+26
View File
@@ -0,0 +1,26 @@
{
"slug": "kleiner-teddy",
"name": {
"de": "Kleiner Teddy",
"en": "Small Teddy",
"ch": "Chline Teddy",
"fr": "Petit ours en peluche"
},
"artikelnummer": "PRAEMIE-TEDDY-KLEIN",
"kategorie": "diy-plushies",
"preis": 12.9,
"bestand": 999,
"beschreibung": {
"de": "Unser handgemachter kleiner Teddy — als Dankeschön-Prämie im Abo-System kostenlos einlösbar, sobald du den passenden Meilenstein erreicht hast.",
"en": "Our handmade small teddy — redeemable for free as a subscription reward once you reach the matching milestone.",
"ch": "Öise vo Hang gmachti chlini Teddy — als Dankeschön-Prämie im Abo-System gratis ilösbar, sobald du dr passendi Meilestei erreicht hesch.",
"fr": "Notre petit ours en peluche fait main — à échanger gratuitement comme récompense d'abonnement une fois le palier atteint."
},
"material": "Baumwollstoff, Füllwatte OEKO-TEX",
"groesse": "ca. 16 cm",
"lieferzeit": "24 Werktage",
"badges": [
"handgemacht"
],
"bilder": []
}
+85
View File
@@ -0,0 +1,85 @@
// Abo-System: Preise/Rabatte/Verhaltensregeln kommen aus dem Adminbereich (Decap CMS, siehe
// public/admin/config.yml → "🎁 Abo-System" → src/content/abo-einstellungen.json). Reine,
// zustandslose Berechnungsfunktionen — der eigentliche Konto-Zustand (welches Abo, wie viele
// bezahlte Monate, freigeschaltete Prämien) lebt in scripts/account.ts.
// @ts-ignore -- JSON-Import, zur Build-Zeit von Astro/Vite aufgelöst
import raw from "../content/abo-einstellungen.json";
export type AboStufe = "klein" | "mittel" | "gross";
export interface AboStufenKonfig {
preis: number;
rabattProzent: number;
}
export interface AboEinstellungen {
klein: AboStufenKonfig;
mittel: AboStufenKonfig;
gross: AboStufenKonfig;
kombinierbarMitAnderenRabatten: boolean;
versandKostenlosBeiPraemie: boolean;
fortsetzungBeiWiederanmeldung: boolean;
monateUeberStufenZusammenzaehlen: boolean;
}
export const aboEinstellungen = raw as AboEinstellungen;
export function aboStufenKonfig(stufe: AboStufe): AboStufenKonfig {
return aboEinstellungen[stufe];
}
export const ABO_STUFEN: AboStufe[] = ["klein", "mittel", "gross"];
/** Wie viele Teddy-Prämien welcher Art bei X durchgehend bezahlten Monaten in dieser Stufe
* INSGESAMT freigeschaltet sein müssten (kumulativ, nicht nur "neu diesen Monat"):
* - Klein-Abo: 1 kleiner Teddy pro volles Jahr.
* - Mittel-Abo: 1 großer Teddy pro volles Jahr.
* - Groß-Abo: 1 kleiner Teddy alle 6 Monate UND zusätzlich 1 großer Teddy pro volles Jahr
* (nach 6 Monaten: 1 klein · nach 12: 2 klein + 1 groß · nach 18: 3 klein · nach 24: 4 klein + 2 groß). */
export function teddyMeilensteine(stufe: AboStufe, monate: number): { klein: number; gross: number } {
if (monate <= 0) return { klein: 0, gross: 0 };
if (stufe === "klein") return { klein: Math.floor(monate / 12), gross: 0 };
if (stufe === "mittel") return { klein: 0, gross: Math.floor(monate / 12) };
return { klein: Math.floor(monate / 6), gross: Math.floor(monate / 12) };
}
/** Wie viele Monate fehlen noch bis zum jeweils nächsten Meilenstein (für die
* Fortschrittsanzeige im Kundenkonto), null wenn diese Stufe die Prämienart gar nicht kennt. */
export function naechsteMeilensteine(stufe: AboStufe, monate: number): { klein: number | null; gross: number | null } {
const kleinIntervall = stufe === "gross" ? 6 : stufe === "klein" ? 12 : null;
const grossIntervall = stufe === "klein" ? null : 12;
return {
klein: kleinIntervall ? kleinIntervall - (monate % kleinIntervall) : null,
gross: grossIntervall ? grossIntervall - (monate % grossIntervall) : null,
};
}
/** Ein Abo ist aktiv, solange der aktuell bezahlte Abrechnungszeitraum noch nicht abgelaufen
* ist — reiner Datumsvergleich zur echten, aktuellen Uhrzeit (läuft also automatisch ab, ganz
* ohne Cronjob/Backend), genau wie befristete Produkt-Rabatte (siehe data/rabatt.ts). Gilt
* bewusst UNABHÄNGIG davon, ob das Abo schon gekündigt wurde — gekündigte Abos behalten ihre
* Vorteile bis zum Ende der bezahlten Laufzeit. */
export function aboAktuellAktiv(bezahltBisDatum: string | null): boolean {
if (!bezahltBisDatum) return false;
return new Date() <= new Date(`${bezahltBisDatum}T23:59:59`);
}
/** Heutiges Datum als lokales Kalenderdatum (NICHT über toISOString(), das auf UTC umrechnet und
* in Zeitzonen östlich von UTC nachts kurzzeitig das falsche — gestrige — Datum liefern würde). */
export function heutigesDatumISO(): string {
const heute = new Date();
const jahr = heute.getFullYear();
const monat = String(heute.getMonth() + 1).padStart(2, "0");
const tag = String(heute.getDate()).padStart(2, "0");
return `${jahr}-${monat}-${tag}`;
}
/** Rechnet rein kalendarisch in UTC (bewusst NICHT über lokale Zeit + toISOString() gemischt —
* das würde je nach Zeitzone des Geräts bei jedem Aufruf einen Tag "verlieren" und sich über
* viele Monate hinweg zu einem spürbaren Datumsfehler aufsummieren; mit reinem UTC-Rechnen ist
* das Ergebnis unabhängig von der Zeitzone des Kunden immer exakt). */
export function addMonateISO(datumISO: string, monate: number): string {
const [jahr, monat, tag] = datumISO.split("-").map(Number);
return new Date(Date.UTC(jahr, monat - 1 + monate, tag)).toISOString().slice(0, 10);
}
+180
View File
@@ -180,6 +180,8 @@ export const ui = {
loyaltyBannerText: "Wähle unten den Artikel aus, auf den du 20 % Rabatt erhältst.", loyaltyBannerText: "Wähle unten den Artikel aus, auf den du 20 % Rabatt erhältst.",
loyaltySelectLabel: "Für 20 % Treuebonus auswählen", loyaltySelectLabel: "Für 20 % Treuebonus auswählen",
loyaltySelectedLabel: "🎁 -20 % Treuebonus angewendet", loyaltySelectedLabel: "🎁 -20 % Treuebonus angewendet",
gratisPraemie: "Gratis-Prämie aus deinem Abo",
aboDiscount: "🎫 Abo-Rabatt",
}, },
checkout: { checkout: {
eyebrow: "Checkout", title: "Bestellung abschließen", eyebrow: "Checkout", title: "Bestellung abschließen",
@@ -238,6 +240,49 @@ export const ui = {
loyaltyUnlockedText: "Du hast zehn Bestellungen abgeschlossen und erhältst bei deiner nächsten Bestellung 20 % Rabatt auf einen Artikel deiner Wahl.", loyaltyUnlockedText: "Du hast zehn Bestellungen abgeschlossen und erhältst bei deiner nächsten Bestellung 20 % Rabatt auf einen Artikel deiner Wahl.",
loyaltyGoToCart: "Artikel im Warenkorb auswählen →", loyaltyGoToCart: "Artikel im Warenkorb auswählen →",
loyaltyExplainer: "Es zählen nur vollständig abgeschlossene und bezahlte Bestellungen. Nach der Einlösung beginnt dein Fortschritt wieder bei null.", loyaltyExplainer: "Es zählen nur vollständig abgeschlossene und bezahlte Bestellungen. Nach der Einlösung beginnt dein Fortschritt wieder bei null.",
statAbo: "Mein Abo",
aboHeaderBtn: "🎫 Mein Abo verwalten",
aboTitle: "🎫 Mein Abo",
aboIntro: "Sichere dir dauerhaften Rabatt auf jede Bestellung und hol dir kostenlose Teddys als Dankeschön — automatisch, jeden Monat.",
aboNameKlein: "Kleines Abo", aboNameMittel: "Mittleres Abo", aboNameGross: "Großes Abo",
aboPreisSuffix: "/ Monat",
aboVorteilKlein: "5 % Rabatt auf jede Bestellung. Nach jedem vollen Abo-Jahr: 1 kleiner Teddy gratis.",
aboVorteilMittel: "10 % Rabatt auf jede Bestellung. Nach jedem vollen Abo-Jahr: 1 großer Teddy gratis.",
aboVorteilGross: "20 % Rabatt auf jede Bestellung. Alle 6 Monate ein kleiner Teddy, nach jedem vollen Jahr zusätzlich ein großer Teddy.",
aboAbschliessenBtn: "Abo abschließen",
aboAktivesLabel: "✓ Dein aktuelles Abo",
aboWechselnBtn: "Zu diesem Abo wechseln",
aboStatusTitle: "Dein Abo-Status",
aboFieldStufe: "Abo-Stufe", aboFieldPreis: "Monatlicher Preis", aboFieldRabatt: "Enthaltener Rabatt",
aboFieldStart: "Startdatum", aboFieldNaechsteZahlung: "Nächste Zahlung", aboFieldLaufzeit: "Bisherige Laufzeit",
aboMonateSuffix: "Monate",
aboFortschrittKleinTemplate: "🧸 Noch {n} Monat(e) bis zum nächsten kleinen Teddy",
aboFortschrittGrossTemplate: "🧸 Noch {n} Monat(e) bis zum nächsten großen Teddy",
aboPraemienErhaltenTemplate: "Bereits erhalten: {klein}× kleiner Teddy, {gross}× großer Teddy",
aboPraemienVerfuegbarTitle: "🎉 Verfügbare Prämien",
aboKeinePraemienVerfuegbar: "Aktuell keine Prämie zum Einlösen verfügbar.",
aboEinloesenBtn: "Jetzt einlösen",
aboEinloesenKleinLabel: "Kleiner Teddy", aboEinloesenGrossLabel: "Großer Teddy",
aboEingeloestHinweis: "🧸 In den Warenkorb gelegt!",
aboZahlungSimBtn: "Monatszahlung simulieren (Demo)",
aboZahlungSimHinweis: "Phase 1: Es gibt noch keine echte wiederkehrende Zahlung im Hintergrund. Mit diesem Knopf testest du, was bei der nächsten erfolgreichen Monatsabbuchung passiert.",
aboKuendigenBtn: "Abo kündigen",
aboKuendigenConfirm: "Abo wirklich kündigen? Deine Vorteile bleiben bis zum Ende der bezahlten Laufzeit aktiv.",
aboGekuendigtHinweisTemplate: "Gekündigt am {datum}. Deine Vorteile bleiben bis zum Ende der bezahlten Laufzeit aktiv, danach endet dein Abo automatisch.",
aboWechselTitle: "Zu einem anderen Abo wechseln",
aboBenachrichtigungenTitle: "🔔 Benachrichtigungen",
aboKeineBenachrichtigungen: "Noch keine Benachrichtigungen.",
aboNichtAktivBadge: "⏸ Rabatt pausiert",
aboExplainer: "Alle Angaben werden aktuell sicher auf diesem Gerät gespeichert (Phase 1). Eine geräteübergreifende Datenbank folgt mit der echten Zahlungsanbindung in Phase 2.",
notifAboAbgeschlossen: "Dein Abo wurde erfolgreich abgeschlossen — willkommen! 🎉",
notifAboGewechselt: "Du hast erfolgreich in ein anderes Abo gewechselt ({stufe}).",
notifAboZahlungErfolgreich: "Deine monatliche Abo-Zahlung war erfolgreich. ✅",
notifAboTeddyKleinFrei: "🧸 Neue Prämie freigeschaltet: ein kleiner Teddy wartet auf dich!",
notifAboTeddyGrossFrei: "🧸 Neue Prämie freigeschaltet: ein großer Teddy wartet auf dich!",
notifAboBaldPraemie: "🧸 Fast geschafft — nächsten Monat wartet deine nächste Teddy-Prämie!",
notifAboGekuendigt: "Du hast dein Abo gekündigt. Deine Vorteile bleiben bis zum Ende der bezahlten Laufzeit aktiv.",
notifAboTeddyKleinEingeloest: "🧸 Kleiner Teddy eingelöst — er liegt jetzt kostenlos in deinem Warenkorb.",
notifAboTeddyGrossEingeloest: "🧸 Großer Teddy eingelöst — er liegt jetzt kostenlos in deinem Warenkorb.",
noOpenOrders: "Aktuell keine offenen Sendungen — alle Bestellungen sind schon angekommen. 🎉", noOpenOrders: "Aktuell keine offenen Sendungen — alle Bestellungen sind schon angekommen. 🎉",
noArrivedOrders: "Noch keine angekommenen Bestellungen — hier erscheinen deine Bestellungen, sobald sie geliefert wurden.", noArrivedOrders: "Noch keine angekommenen Bestellungen — hier erscheinen deine Bestellungen, sobald sie geliefert wurden.",
ordersTitle: "📦 Meine Bestellungen", ordersTitle: "📦 Meine Bestellungen",
@@ -431,6 +476,8 @@ export const ui = {
loyaltyBannerText: "Pick the item below that you'd like 20% off.", loyaltyBannerText: "Pick the item below that you'd like 20% off.",
loyaltySelectLabel: "Select for 20% loyalty reward", loyaltySelectLabel: "Select for 20% loyalty reward",
loyaltySelectedLabel: "🎁 -20% loyalty reward applied", loyaltySelectedLabel: "🎁 -20% loyalty reward applied",
gratisPraemie: "Free reward from your subscription",
aboDiscount: "🎫 Subscription discount",
}, },
checkout: { checkout: {
eyebrow: "Checkout", title: "Complete your order", eyebrow: "Checkout", title: "Complete your order",
@@ -489,6 +536,49 @@ export const ui = {
loyaltyUnlockedText: "You've completed ten orders and get 20% off an item of your choice on your next order.", loyaltyUnlockedText: "You've completed ten orders and get 20% off an item of your choice on your next order.",
loyaltyGoToCart: "Choose item in cart →", loyaltyGoToCart: "Choose item in cart →",
loyaltyExplainer: "Only fully completed and paid orders count. After redemption your progress starts again from zero.", loyaltyExplainer: "Only fully completed and paid orders count. After redemption your progress starts again from zero.",
statAbo: "My Subscription",
aboHeaderBtn: "🎫 Manage my subscription",
aboTitle: "🎫 My Subscription",
aboIntro: "Get a permanent discount on every order and free teddies as a thank-you — automatically, every month.",
aboNameKlein: "Small plan", aboNameMittel: "Medium plan", aboNameGross: "Large plan",
aboPreisSuffix: "/ month",
aboVorteilKlein: "5% off every order. After each full subscription year: 1 small teddy for free.",
aboVorteilMittel: "10% off every order. After each full subscription year: 1 large teddy for free.",
aboVorteilGross: "20% off every order. A small teddy every 6 months, plus a large teddy after every full year.",
aboAbschliessenBtn: "Subscribe",
aboAktivesLabel: "✓ Your current plan",
aboWechselnBtn: "Switch to this plan",
aboStatusTitle: "Your subscription status",
aboFieldStufe: "Plan", aboFieldPreis: "Monthly price", aboFieldRabatt: "Included discount",
aboFieldStart: "Start date", aboFieldNaechsteZahlung: "Next payment", aboFieldLaufzeit: "Duration so far",
aboMonateSuffix: "months",
aboFortschrittKleinTemplate: "🧸 {n} more month(s) until your next small teddy",
aboFortschrittGrossTemplate: "🧸 {n} more month(s) until your next large teddy",
aboPraemienErhaltenTemplate: "Already received: {klein}× small teddy, {gross}× large teddy",
aboPraemienVerfuegbarTitle: "🎉 Available rewards",
aboKeinePraemienVerfuegbar: "No reward available to redeem right now.",
aboEinloesenBtn: "Redeem now",
aboEinloesenKleinLabel: "Small teddy", aboEinloesenGrossLabel: "Large teddy",
aboEingeloestHinweis: "🧸 Added to your cart!",
aboZahlungSimBtn: "Simulate monthly payment (demo)",
aboZahlungSimHinweis: "Phase 1: there is no real recurring payment running in the background yet. Use this button to test what happens on the next successful monthly payment.",
aboKuendigenBtn: "Cancel subscription",
aboKuendigenConfirm: "Really cancel your subscription? Your benefits stay active until the end of the paid period.",
aboGekuendigtHinweisTemplate: "Cancelled on {datum}. Your benefits stay active until the end of the paid period, then your subscription ends automatically.",
aboWechselTitle: "Switch to a different plan",
aboBenachrichtigungenTitle: "🔔 Notifications",
aboKeineBenachrichtigungen: "No notifications yet.",
aboNichtAktivBadge: "⏸ Discount paused",
aboExplainer: "All data is currently stored securely on this device (Phase 1). A cross-device database follows with real payment integration in Phase 2.",
notifAboAbgeschlossen: "Your subscription was successfully set up — welcome! 🎉",
notifAboGewechselt: "You successfully switched to a different plan ({stufe}).",
notifAboZahlungErfolgreich: "Your monthly subscription payment was successful. ✅",
notifAboTeddyKleinFrei: "🧸 New reward unlocked: a small teddy is waiting for you!",
notifAboTeddyGrossFrei: "🧸 New reward unlocked: a large teddy is waiting for you!",
notifAboBaldPraemie: "🧸 Almost there — your next teddy reward is waiting next month!",
notifAboGekuendigt: "You cancelled your subscription. Your benefits stay active until the end of the paid period.",
notifAboTeddyKleinEingeloest: "🧸 Small teddy redeemed — it's now in your cart for free.",
notifAboTeddyGrossEingeloest: "🧸 Large teddy redeemed — it's now in your cart for free.",
noOpenOrders: "No open shipments right now — every order has already arrived. 🎉", noOpenOrders: "No open shipments right now — every order has already arrived. 🎉",
noArrivedOrders: "No delivered orders yet — your orders will appear here once they've arrived.", noArrivedOrders: "No delivered orders yet — your orders will appear here once they've arrived.",
ordersTitle: "📦 My orders", ordersTitle: "📦 My orders",
@@ -682,6 +772,8 @@ export const ui = {
loyaltyBannerText: "Wähl unde dr Artikel us, uf dä du 20 % Rabatt übercho.", loyaltyBannerText: "Wähl unde dr Artikel us, uf dä du 20 % Rabatt übercho.",
loyaltySelectLabel: "Für 20 % Treuebonus uswähle", loyaltySelectLabel: "Für 20 % Treuebonus uswähle",
loyaltySelectedLabel: "🎁 -20 % Treuebonus aagwändet", loyaltySelectedLabel: "🎁 -20 % Treuebonus aagwändet",
gratisPraemie: "Gratisprämie us dim Abo",
aboDiscount: "🎫 Abo-Rabatt",
}, },
checkout: { checkout: {
eyebrow: "Checkout", title: "Bschtellig abschliesse", eyebrow: "Checkout", title: "Bschtellig abschliesse",
@@ -740,6 +832,49 @@ export const ui = {
loyaltyUnlockedText: "Du hesch zäh Bstellige abgschlosse und übercho bi dinere nächschte Bstellig 20 % Rabatt uf en Artikel dinere Wahl.", loyaltyUnlockedText: "Du hesch zäh Bstellige abgschlosse und übercho bi dinere nächschte Bstellig 20 % Rabatt uf en Artikel dinere Wahl.",
loyaltyGoToCart: "Artikel im Warenchorb uswähle →", loyaltyGoToCart: "Artikel im Warenchorb uswähle →",
loyaltyExplainer: "Es zelle nume vollständig abgschlosseni und bezahlti Bstellige. Nach dr Ilösig fangt din Fortschritt wieder bi null a.", loyaltyExplainer: "Es zelle nume vollständig abgschlosseni und bezahlti Bstellige. Nach dr Ilösig fangt din Fortschritt wieder bi null a.",
statAbo: "Mis Abo",
aboHeaderBtn: "🎫 Mis Abo verwalte",
aboTitle: "🎫 Mis Abo",
aboIntro: "Sicher dir en dauerhafte Rabatt uf jedi Bstellig und hol dir gratis Teddys als Dankeschön — automatisch, jede Monet.",
aboNameKlein: "Chliises Abo", aboNameMittel: "Mittleres Abo", aboNameGross: "Grosses Abo",
aboPreisSuffix: "/ Monet",
aboVorteilKlein: "5 % Rabatt uf jedi Bstellig. Nach jedem volle Abo-Jahr: 1 chliine Teddy gratis.",
aboVorteilMittel: "10 % Rabatt uf jedi Bstellig. Nach jedem volle Abo-Jahr: 1 grosse Teddy gratis.",
aboVorteilGross: "20 % Rabatt uf jedi Bstellig. Alli 6 Monet en chliine Teddy, nach jedem volle Jahr zuesätzlich en grosse Teddy.",
aboAbschliessenBtn: "Abo abschliesse",
aboAktivesLabel: "✓ Dis aktuells Abo",
aboWechselnBtn: "Zu dem Abo wechsle",
aboStatusTitle: "Din Abo-Status",
aboFieldStufe: "Abo-Stufe", aboFieldPreis: "Monetliche Priis", aboFieldRabatt: "Enthaltene Rabatt",
aboFieldStart: "Startdatum", aboFieldNaechsteZahlung: "Nächschti Zahlig", aboFieldLaufzeit: "Bisherigi Laufziit",
aboMonateSuffix: "Monet",
aboFortschrittKleinTemplate: "🧸 No {n} Monet bis zum nächschte chliine Teddy",
aboFortschrittGrossTemplate: "🧸 No {n} Monet bis zum nächschte grosse Teddy",
aboPraemienErhaltenTemplate: "Scho erhalte: {klein}× chliine Teddy, {gross}× grosse Teddy",
aboPraemienVerfuegbarTitle: "🎉 Verfüegbari Prämie",
aboKeinePraemienVerfuegbar: "Momentan kei Prämie zum Ilöse verfüegbar.",
aboEinloesenBtn: "Jetzt ilöse",
aboEinloesenKleinLabel: "Chliine Teddy", aboEinloesenGrossLabel: "Grosse Teddy",
aboEingeloestHinweis: "🧸 In Warechorb gleit!",
aboZahlungSimBtn: "Monetszahlig simuliere (Demo)",
aboZahlungSimHinweis: "Phase 1: Es git no kei echti wiederkehrendi Zahlig im Hintergrund. Mit dem Chnopf teschtisch, was bi dr nächschte erfolgriiche Monetsabbuechig passiert.",
aboKuendigenBtn: "Abo kündige",
aboKuendigenConfirm: "Abo würklich kündige? Dini Vorteil bliibed bis am Ändi vo dr bezahlte Laufziit aktiv.",
aboGekuendigtHinweisTemplate: "Kündet am {datum}. Dini Vorteil bliibed bis am Ändi vo dr bezahlte Laufziit aktiv, dernah ändet dis Abo automatisch.",
aboWechselTitle: "Zu emene andere Abo wechsle",
aboBenachrichtigungenTitle: "🔔 Benachrichtigunge",
aboKeineBenachrichtigungen: "No kei Benachrichtigunge.",
aboNichtAktivBadge: "⏸ Rabatt pausiert",
aboExplainer: "Alli Aagabe werded aktuell sicher uf dem Gerät gspeicheret (Phase 1). Ä geräteübergriifendi Datebank chunt mit dr echte Zahligs-Aabindig i Phase 2.",
notifAboAbgeschlossen: "Dis Abo isch erfolgriich abgschlosse worde — willkomme! 🎉",
notifAboGewechselt: "Du bisch erfolgriich in es anders Abo gwechslet ({stufe}).",
notifAboZahlungErfolgreich: "Dini monetlichi Abo-Zahlig isch erfolgriich gsi. ✅",
notifAboTeddyKleinFrei: "🧸 Nöii Prämie freigschaltet: en chliine Teddy wartet uf dich!",
notifAboTeddyGrossFrei: "🧸 Nöii Prämie freigschaltet: en grosse Teddy wartet uf dich!",
notifAboBaldPraemie: "🧸 Fasch gschafft — nächschte Monet wartet dini nächschti Teddy-Prämie!",
notifAboGekuendigt: "Du hesch dis Abo kündet. Dini Vorteil bliibed bis am Ändi vo dr bezahlte Laufziit aktiv.",
notifAboTeddyKleinEingeloest: "🧸 Chliine Teddy ilöst — er liit jetzt gratis in dim Warechorb.",
notifAboTeddyGrossEingeloest: "🧸 Grosse Teddy ilöst — er liit jetzt gratis in dim Warechorb.",
noOpenOrders: "Momentan kei offni Sändige — alli Bstellige sind scho aacho. 🎉", noOpenOrders: "Momentan kei offni Sändige — alli Bstellige sind scho aacho. 🎉",
noArrivedOrders: "No kei aacho Bstellige — da erschine dini Bstellige, sobald sie glifert worde sind.", noArrivedOrders: "No kei aacho Bstellige — da erschine dini Bstellige, sobald sie glifert worde sind.",
ordersTitle: "📦 Mini Bstellige", ordersTitle: "📦 Mini Bstellige",
@@ -933,6 +1068,8 @@ export const ui = {
loyaltyBannerText: "Choisissez ci-dessous l'article sur lequel vous voulez -20 %.", loyaltyBannerText: "Choisissez ci-dessous l'article sur lequel vous voulez -20 %.",
loyaltySelectLabel: "Choisir pour la récompense fidélité de 20 %", loyaltySelectLabel: "Choisir pour la récompense fidélité de 20 %",
loyaltySelectedLabel: "🎁 Récompense fidélité de -20 % appliquée", loyaltySelectedLabel: "🎁 Récompense fidélité de -20 % appliquée",
gratisPraemie: "Récompense gratuite de votre abonnement",
aboDiscount: "🎫 Remise d'abonnement",
}, },
checkout: { checkout: {
eyebrow: "Paiement", title: "Finaliser la commande", eyebrow: "Paiement", title: "Finaliser la commande",
@@ -991,6 +1128,49 @@ export const ui = {
loyaltyUnlockedText: "Vous avez validé dix commandes et bénéficiez de 20 % de réduction sur l'article de votre choix lors de votre prochaine commande.", loyaltyUnlockedText: "Vous avez validé dix commandes et bénéficiez de 20 % de réduction sur l'article de votre choix lors de votre prochaine commande.",
loyaltyGoToCart: "Choisir l'article dans le panier →", loyaltyGoToCart: "Choisir l'article dans le panier →",
loyaltyExplainer: "Seules les commandes entièrement finalisées et payées comptent. Après utilisation, votre progression repart de zéro.", loyaltyExplainer: "Seules les commandes entièrement finalisées et payées comptent. Après utilisation, votre progression repart de zéro.",
statAbo: "Mon abonnement",
aboHeaderBtn: "🎫 Gérer mon abonnement",
aboTitle: "🎫 Mon abonnement",
aboIntro: "Profitez d'une remise permanente sur chaque commande et d'oursons en peluche gratuits en cadeau — automatiquement, chaque mois.",
aboNameKlein: "Petit abonnement", aboNameMittel: "Abonnement moyen", aboNameGross: "Grand abonnement",
aboPreisSuffix: "/ mois",
aboVorteilKlein: "5 % de remise sur chaque commande. Après chaque année d'abonnement complète : 1 petit ours en peluche offert.",
aboVorteilMittel: "10 % de remise sur chaque commande. Après chaque année d'abonnement complète : 1 grand ours en peluche offert.",
aboVorteilGross: "20 % de remise sur chaque commande. Un petit ours tous les 6 mois, et un grand ours en plus après chaque année complète.",
aboAbschliessenBtn: "S'abonner",
aboAktivesLabel: "✓ Votre abonnement actuel",
aboWechselnBtn: "Passer à cet abonnement",
aboStatusTitle: "Statut de votre abonnement",
aboFieldStufe: "Formule", aboFieldPreis: "Prix mensuel", aboFieldRabatt: "Remise incluse",
aboFieldStart: "Date de début", aboFieldNaechsteZahlung: "Prochain paiement", aboFieldLaufzeit: "Durée jusqu'ici",
aboMonateSuffix: "mois",
aboFortschrittKleinTemplate: "🧸 Encore {n} mois avant votre prochain petit ours",
aboFortschrittGrossTemplate: "🧸 Encore {n} mois avant votre prochain grand ours",
aboPraemienErhaltenTemplate: "Déjà reçus : {klein}× petit ours, {gross}× grand ours",
aboPraemienVerfuegbarTitle: "🎉 Récompenses disponibles",
aboKeinePraemienVerfuegbar: "Aucune récompense à échanger pour le moment.",
aboEinloesenBtn: "Échanger maintenant",
aboEinloesenKleinLabel: "Petit ours", aboEinloesenGrossLabel: "Grand ours",
aboEingeloestHinweis: "🧸 Ajouté à votre panier !",
aboZahlungSimBtn: "Simuler le paiement mensuel (démo)",
aboZahlungSimHinweis: "Phase 1 : il n'y a pas encore de vrai paiement récurrent en arrière-plan. Ce bouton permet de tester ce qui se passe lors du prochain paiement mensuel réussi.",
aboKuendigenBtn: "Résilier l'abonnement",
aboKuendigenConfirm: "Résilier vraiment l'abonnement ? Vos avantages restent actifs jusqu'à la fin de la période payée.",
aboGekuendigtHinweisTemplate: "Résilié le {datum}. Vos avantages restent actifs jusqu'à la fin de la période payée, puis votre abonnement se termine automatiquement.",
aboWechselTitle: "Passer à une autre formule",
aboBenachrichtigungenTitle: "🔔 Notifications",
aboKeineBenachrichtigungen: "Pas encore de notifications.",
aboNichtAktivBadge: "⏸ Remise suspendue",
aboExplainer: "Toutes les données sont actuellement stockées en sécurité sur cet appareil (Phase 1). Une base de données multi-appareils suivra avec le vrai paiement en Phase 2.",
notifAboAbgeschlossen: "Votre abonnement a été souscrit avec succès — bienvenue ! 🎉",
notifAboGewechselt: "Vous êtes passé(e) avec succès à un autre abonnement ({stufe}).",
notifAboZahlungErfolgreich: "Votre paiement mensuel d'abonnement a été effectué avec succès. ✅",
notifAboTeddyKleinFrei: "🧸 Nouvelle récompense débloquée : un petit ours vous attend !",
notifAboTeddyGrossFrei: "🧸 Nouvelle récompense débloquée : un grand ours vous attend !",
notifAboBaldPraemie: "🧸 Presque au but — votre prochaine récompense en peluche arrive le mois prochain !",
notifAboGekuendigt: "Vous avez résilié votre abonnement. Vos avantages restent actifs jusqu'à la fin de la période payée.",
notifAboTeddyKleinEingeloest: "🧸 Petit ours échangé — il est maintenant gratuit dans votre panier.",
notifAboTeddyGrossEingeloest: "🧸 Grand ours échangé — il est maintenant gratuit dans votre panier.",
noOpenOrders: "Aucun envoi en cours pour le moment — toutes les commandes sont déjà arrivées. 🎉", noOpenOrders: "Aucun envoi en cours pour le moment — toutes les commandes sont déjà arrivées. 🎉",
noArrivedOrders: "Aucune commande livrée pour le moment — vos commandes apparaîtront ici dès qu'elles seront arrivées.", noArrivedOrders: "Aucune commande livrée pour le moment — vos commandes apparaîtront ici dès qu'elles seront arrivées.",
ordersTitle: "📦 Mes commandes", ordersTitle: "📦 Mes commandes",
+8 -8
View File
@@ -173,20 +173,19 @@ const t = useTranslations(lang);
</section> </section>
</Layout> </Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/ch/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, 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. // 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>
<script type="module"> <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 { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products"; import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand"; import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis } from "../../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung"; import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account"; 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 summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land"); const landSelect = document.getElementById("land");
@@ -204,18 +203,19 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const partnerCode = appliedPartnerCode(); const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal); const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount(); const loyaltyDiscount = treuebonusDiscount();
const aboDiscountBetrag = aboDiscount(subtotal);
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping; const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total; currentTotal = total;
summary.innerHTML = ` summary.innerHTML = `
${cart.map((i) => { ${cart.map((i) => {
const produkt = products.find((p) => p.slug === i.slug); const zeilenpreis = zeilenpreisFuer(i);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis; return `<div class="row"><span>${i.menge}× ${i.name}${i.gratis ? ` <span class="small">(${gratisPraemieLabel})</span>` : ""}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
}).join("")} }).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""} ${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""} ${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""} ${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"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div> <div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
`; `;
+254 -5
View File
@@ -5,6 +5,7 @@ import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config"; import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui"; import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format"; import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo";
const lang: Locale = "ch"; const lang: Locale = "ch";
const t = useTranslations(lang); const t = useTranslations(lang);
@@ -49,6 +50,14 @@ const statusLabel = {
abgeschlossen: t.accountDash.statusAbgeschlossen, abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const; } 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 — // "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. // in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3); const zuletztAngesehen = products.slice(0, 3);
@@ -79,7 +88,13 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<p class="lead">{t.accountDash.lead}</p> <p class="lead">{t.accountDash.lead}</p>
</div> </div>
</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> </div>
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" / {/* 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-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span> <span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button> </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> </nav>
</div> </div>
</section> </section>
@@ -314,10 +335,83 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div> </div>
</div> </div>
</section> </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> </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}}> <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 }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script> </script>
<script> <script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den // Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -331,6 +425,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const recentSection = document.getElementById("zuletzt-angesehen"); const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten"); const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus"); const treuebonusSection = document.getElementById("treuebonus");
const aboSection = document.getElementById("abo");
const orderCards = Array.from(document.querySelectorAll(".order-card")); const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null; const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -343,6 +438,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (wunschlisteSection) wunschlisteSection.style.display = "none"; if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none"; if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none"; if (treuebonusSection) treuebonusSection.style.display = "none";
if (aboSection) aboSection.style.display = "none";
if (recentSection) recentSection.style.display = "none"; if (recentSection) recentSection.style.display = "none";
if (tab === null) { if (tab === null) {
@@ -365,6 +461,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return; return;
} }
if (tab === "abo") {
if (aboSection) aboSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = ""; if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0; let visibleCount = 0;
orderCards.forEach((card) => { orderCards.forEach((card) => {
@@ -387,12 +488,20 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); }); 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); setActiveTab(null);
</script> </script>
<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"; 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 // Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per // echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -466,6 +575,146 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderLoyalty(); renderLoyalty();
window.addEventListener("account:changed", 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 // Benutzername ändern
const saveBtn = document.getElementById("profile-username-save"); const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null; const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+21 -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="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div> <div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div> <div class="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"> <div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label> <label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group"> <div class="coupon-input-group">
@@ -77,19 +78,19 @@ const t = useTranslations(lang);
</section> </section>
</Layout> </Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, 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. // 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>
<script type="module"> <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 { formatPrice } from "../../../i18n/format";
import { getProduct, products } from "../../../data/products"; import { getProduct, products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand"; import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt"; import { mengenrabattProzent } from "../../../data/rabatt";
import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scripts/account"; 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"); const landSelect = document.getElementById("cart-land");
function render() { function render() {
@@ -108,7 +109,7 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
const zeilen = cart.map((i) => { const zeilen = cart.map((i) => {
const produkt = getProduct(i.slug); 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; const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
return { item: i, produkt, zeilenpreis, mengenProzent }; return { item: i, produkt, zeilenpreis, mengenProzent };
}); });
@@ -133,15 +134,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
const loyaltyControl = !treueAktiv ? "" : istTreueartikel const loyaltyControl = !treueAktiv ? "" : istTreueartikel
? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>` ? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>`
: `<label class="loyalty-select"><input type="radio" name="loyalty-artikel" data-loyalty-slug="${i.slug}" /> ${loyaltySelectLabel}</label>`; : `<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 ` return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}"> <div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" /> <input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
${thumb} ${thumb}
<div class="info"> <div class="info">
<strong>${i.name}</strong> <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} ${mengenBadge}
${keptBadge} ${keptBadge}
${gratisBadge}
${loyaltyControl} ${loyaltyControl}
</div> </div>
<div class="qty-controls"> <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) => { itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => { el.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
+8 -8
View File
@@ -173,23 +173,22 @@ const t = useTranslations(lang);
</section> </section>
</Layout> </Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, 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 // WICHTIG: define:vars-Skripte werden von Astro in ein IIFE gepackt (kein ES-Modul) — echte
// `import`-Anweisungen würden hier zur Laufzeit mit "Cannot use import statement outside a // `import`-Anweisungen würden hier zur Laufzeit mit "Cannot use import statement outside a
// module" fehlschlagen. Deshalb hier nur die vom Server gerenderten Werte auf window ablegen, // module" fehlschlagen. Deshalb hier nur die vom Server gerenderten Werte auf window ablegen,
// die eigentliche Logik läuft im separaten <script type="module"> darunter. // die eigentliche Logik läuft im separaten <script type="module"> darunter.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, 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>
<script type="module"> <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 { formatPrice } from "../../i18n/format";
import { products } from "../../data/products"; import { products } from "../../data/products";
import { berechneVersandkosten } from "../../data/versand"; import { berechneVersandkosten } from "../../data/versand";
import { effektiverZeilenpreis } from "../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbindung"; import { bankverbindung, bankverbindungVollstaendig } from "../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../scripts/account"; 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 summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land"); const landSelect = document.getElementById("land");
@@ -207,18 +206,19 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const partnerCode = appliedPartnerCode(); const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal); const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount(); const loyaltyDiscount = treuebonusDiscount();
const aboDiscountBetrag = aboDiscount(subtotal);
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping; const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total; currentTotal = total;
summary.innerHTML = ` summary.innerHTML = `
${cart.map((i) => { ${cart.map((i) => {
const produkt = products.find((p) => p.slug === i.slug); const zeilenpreis = zeilenpreisFuer(i);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis; return `<div class="row"><span>${i.menge}× ${i.name}${i.gratis ? ` <span class="small">(${gratisPraemieLabel})</span>` : ""}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
}).join("")} }).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""} ${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""} ${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""} ${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"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div> <div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
`; `;
+8 -8
View File
@@ -173,20 +173,19 @@ const t = useTranslations(lang);
</section> </section>
</Layout> </Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/en/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, 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. // 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>
<script type="module"> <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 { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products"; import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand"; import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis } from "../../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung"; import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account"; 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 summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land"); const landSelect = document.getElementById("land");
@@ -204,18 +203,19 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const partnerCode = appliedPartnerCode(); const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal); const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount(); const loyaltyDiscount = treuebonusDiscount();
const aboDiscountBetrag = aboDiscount(subtotal);
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping; const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total; currentTotal = total;
summary.innerHTML = ` summary.innerHTML = `
${cart.map((i) => { ${cart.map((i) => {
const produkt = products.find((p) => p.slug === i.slug); const zeilenpreis = zeilenpreisFuer(i);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis; return `<div class="row"><span>${i.menge}× ${i.name}${i.gratis ? ` <span class="small">(${gratisPraemieLabel})</span>` : ""}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
}).join("")} }).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""} ${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""} ${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""} ${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"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div> <div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
`; `;
+254 -5
View File
@@ -5,6 +5,7 @@ import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config"; import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui"; import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format"; import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo";
const lang: Locale = "en"; const lang: Locale = "en";
const t = useTranslations(lang); const t = useTranslations(lang);
@@ -49,6 +50,14 @@ const statusLabel = {
abgeschlossen: t.accountDash.statusAbgeschlossen, abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const; } 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 — // "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. // in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3); const zuletztAngesehen = products.slice(0, 3);
@@ -79,7 +88,13 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<p class="lead">{t.accountDash.lead}</p> <p class="lead">{t.accountDash.lead}</p>
</div> </div>
</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> </div>
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" / {/* 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-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span> <span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button> </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> </nav>
</div> </div>
</section> </section>
@@ -314,10 +335,83 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div> </div>
</div> </div>
</section> </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> </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}}> <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 }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script> </script>
<script> <script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den // Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -331,6 +425,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const recentSection = document.getElementById("zuletzt-angesehen"); const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten"); const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus"); const treuebonusSection = document.getElementById("treuebonus");
const aboSection = document.getElementById("abo");
const orderCards = Array.from(document.querySelectorAll(".order-card")); const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null; const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -343,6 +438,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (wunschlisteSection) wunschlisteSection.style.display = "none"; if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none"; if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none"; if (treuebonusSection) treuebonusSection.style.display = "none";
if (aboSection) aboSection.style.display = "none";
if (recentSection) recentSection.style.display = "none"; if (recentSection) recentSection.style.display = "none";
if (tab === null) { if (tab === null) {
@@ -365,6 +461,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return; return;
} }
if (tab === "abo") {
if (aboSection) aboSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = ""; if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0; let visibleCount = 0;
orderCards.forEach((card) => { orderCards.forEach((card) => {
@@ -387,12 +488,20 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); }); 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); setActiveTab(null);
</script> </script>
<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"; 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 // Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per // echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -466,6 +575,146 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderLoyalty(); renderLoyalty();
window.addEventListener("account:changed", 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 // Benutzername ändern
const saveBtn = document.getElementById("profile-username-save"); const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null; const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+21 -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="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div> <div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div> <div class="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"> <div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label> <label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group"> <div class="coupon-input-group">
@@ -77,19 +78,19 @@ const t = useTranslations(lang);
</section> </section>
</Layout> </Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, 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. // 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>
<script type="module"> <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 { formatPrice } from "../../../i18n/format";
import { getProduct, products } from "../../../data/products"; import { getProduct, products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand"; import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt"; import { mengenrabattProzent } from "../../../data/rabatt";
import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scripts/account"; 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"); const landSelect = document.getElementById("cart-land");
function render() { function render() {
@@ -108,7 +109,7 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
const zeilen = cart.map((i) => { const zeilen = cart.map((i) => {
const produkt = getProduct(i.slug); 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; const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
return { item: i, produkt, zeilenpreis, mengenProzent }; return { item: i, produkt, zeilenpreis, mengenProzent };
}); });
@@ -133,15 +134,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
const loyaltyControl = !treueAktiv ? "" : istTreueartikel const loyaltyControl = !treueAktiv ? "" : istTreueartikel
? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>` ? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>`
: `<label class="loyalty-select"><input type="radio" name="loyalty-artikel" data-loyalty-slug="${i.slug}" /> ${loyaltySelectLabel}</label>`; : `<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 ` return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}"> <div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" /> <input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
${thumb} ${thumb}
<div class="info"> <div class="info">
<strong>${i.name}</strong> <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} ${mengenBadge}
${keptBadge} ${keptBadge}
${gratisBadge}
${loyaltyControl} ${loyaltyControl}
</div> </div>
<div class="qty-controls"> <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) => { itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => { el.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
+8 -8
View File
@@ -173,20 +173,19 @@ const t = useTranslations(lang);
</section> </section>
</Layout> </Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/fr/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, 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. // 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>
<script type="module"> <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 { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products"; import { products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand"; import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis } from "../../../data/rabatt";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung"; import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account"; 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 summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land"); const landSelect = document.getElementById("land");
@@ -204,18 +203,19 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const partnerCode = appliedPartnerCode(); const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal); const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount(); const loyaltyDiscount = treuebonusDiscount();
const aboDiscountBetrag = aboDiscount(subtotal);
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping; const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total; currentTotal = total;
summary.innerHTML = ` summary.innerHTML = `
${cart.map((i) => { ${cart.map((i) => {
const produkt = products.find((p) => p.slug === i.slug); const zeilenpreis = zeilenpreisFuer(i);
const zeilenpreis = produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis; return `<div class="row"><span>${i.menge}× ${i.name}${i.gratis ? ` <span class="small">(${gratisPraemieLabel})</span>` : ""}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
return `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
}).join("")} }).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""} ${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""} ${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""} ${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"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div> <div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
`; `;
+254 -5
View File
@@ -5,6 +5,7 @@ import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config"; import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui"; import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format"; import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo";
const lang: Locale = "fr"; const lang: Locale = "fr";
const t = useTranslations(lang); const t = useTranslations(lang);
@@ -49,6 +50,14 @@ const statusLabel = {
abgeschlossen: t.accountDash.statusAbgeschlossen, abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const; } 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 — // "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. // in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3); const zuletztAngesehen = products.slice(0, 3);
@@ -79,7 +88,13 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<p class="lead">{t.accountDash.lead}</p> <p class="lead">{t.accountDash.lead}</p>
</div> </div>
</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> </div>
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" / {/* 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-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span> <span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button> </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> </nav>
</div> </div>
</section> </section>
@@ -314,10 +335,83 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div> </div>
</div> </div>
</section> </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> </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}}> <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 }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script> </script>
<script> <script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den // Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -331,6 +425,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const recentSection = document.getElementById("zuletzt-angesehen"); const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten"); const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus"); const treuebonusSection = document.getElementById("treuebonus");
const aboSection = document.getElementById("abo");
const orderCards = Array.from(document.querySelectorAll(".order-card")); const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null; const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -343,6 +438,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (wunschlisteSection) wunschlisteSection.style.display = "none"; if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none"; if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none"; if (treuebonusSection) treuebonusSection.style.display = "none";
if (aboSection) aboSection.style.display = "none";
if (recentSection) recentSection.style.display = "none"; if (recentSection) recentSection.style.display = "none";
if (tab === null) { if (tab === null) {
@@ -365,6 +461,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return; return;
} }
if (tab === "abo") {
if (aboSection) aboSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = ""; if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0; let visibleCount = 0;
orderCards.forEach((card) => { orderCards.forEach((card) => {
@@ -387,12 +488,20 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); }); 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); setActiveTab(null);
</script> </script>
<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"; 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 // Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per // echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -466,6 +575,146 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderLoyalty(); renderLoyalty();
window.addEventListener("account:changed", 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 // Benutzername ändern
const saveBtn = document.getElementById("profile-username-save"); const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null; const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+21 -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="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div> <div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div> <div class="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"> <div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label> <label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group"> <div class="coupon-input-group">
@@ -77,19 +78,19 @@ const t = useTranslations(lang);
</section> </section>
</Layout> </Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, 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. // 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>
<script type="module"> <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 { formatPrice } from "../../../i18n/format";
import { getProduct, products } from "../../../data/products"; import { getProduct, products } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand"; import { berechneVersandkosten } from "../../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt"; import { mengenrabattProzent } from "../../../data/rabatt";
import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scripts/account"; 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"); const landSelect = document.getElementById("cart-land");
function render() { function render() {
@@ -108,7 +109,7 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
const zeilen = cart.map((i) => { const zeilen = cart.map((i) => {
const produkt = getProduct(i.slug); 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; const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
return { item: i, produkt, zeilenpreis, mengenProzent }; return { item: i, produkt, zeilenpreis, mengenProzent };
}); });
@@ -133,15 +134,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../../scr
const loyaltyControl = !treueAktiv ? "" : istTreueartikel const loyaltyControl = !treueAktiv ? "" : istTreueartikel
? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>` ? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>`
: `<label class="loyalty-select"><input type="radio" name="loyalty-artikel" data-loyalty-slug="${i.slug}" /> ${loyaltySelectLabel}</label>`; : `<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 ` return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}"> <div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" /> <input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
${thumb} ${thumb}
<div class="info"> <div class="info">
<strong>${i.name}</strong> <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} ${mengenBadge}
${keptBadge} ${keptBadge}
${gratisBadge}
${loyaltyControl} ${loyaltyControl}
</div> </div>
<div class="qty-controls"> <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) => { itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => { el.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
+254 -5
View File
@@ -5,6 +5,7 @@ import { products, getProduct } from "../../data/products";
import type { Locale } from "../../i18n/config"; import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui"; import { useTranslations } from "../../i18n/ui";
import { formatPrice } from "../../i18n/format"; import { formatPrice } from "../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../data/abo";
const lang: Locale = "de"; const lang: Locale = "de";
const t = useTranslations(lang); const t = useTranslations(lang);
@@ -49,6 +50,14 @@ const statusLabel = {
abgeschlossen: t.accountDash.statusAbgeschlossen, abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const; } 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 — // "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. // in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3); const zuletztAngesehen = products.slice(0, 3);
@@ -79,7 +88,13 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
<p class="lead">{t.accountDash.lead}</p> <p class="lead">{t.accountDash.lead}</p>
</div> </div>
</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> </div>
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" / {/* 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-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span> <span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button> </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> </nav>
</div> </div>
</section> </section>
@@ -314,10 +335,83 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div> </div>
</div> </div>
</section> </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> </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 }}> <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 }; window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, aboLang, aboLabels };
</script> </script>
<script> <script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den // Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
@@ -331,6 +425,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
const recentSection = document.getElementById("zuletzt-angesehen"); const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten"); const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus"); const treuebonusSection = document.getElementById("treuebonus");
const aboSection = document.getElementById("abo");
const orderCards = Array.from(document.querySelectorAll(".order-card")); const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null; const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
@@ -343,6 +438,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
if (wunschlisteSection) wunschlisteSection.style.display = "none"; if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none"; if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none"; if (treuebonusSection) treuebonusSection.style.display = "none";
if (aboSection) aboSection.style.display = "none";
if (recentSection) recentSection.style.display = "none"; if (recentSection) recentSection.style.display = "none";
if (tab === null) { if (tab === null) {
@@ -365,6 +461,11 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
return; return;
} }
if (tab === "abo") {
if (aboSection) aboSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = ""; if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0; let visibleCount = 0;
orderCards.forEach((card) => { orderCards.forEach((card) => {
@@ -387,12 +488,20 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); }); 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); setActiveTab(null);
</script> </script>
<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"; 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 // Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht. Es gibt noch kein
// echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per // echtes Backend, das den Zugriff serverseitig verweigern könnte (Phase 2) — deshalb hier per
@@ -466,6 +575,146 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
renderLoyalty(); renderLoyalty();
window.addEventListener("account:changed", 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 // Benutzername ändern
const saveBtn = document.getElementById("profile-username-save"); const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null; const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
+21 -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="discount-row" style="display:none;"><span>{t.cart.discount}</span><span id="sum-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div> <div class="summary-row discount-row" id="partner-discount-row" style="display:none;"><span>{t.cart.partnerDiscount}</span><span id="sum-partner-discount">-0,00 €</span></div>
<div class="summary-row discount-row" id="loyalty-discount-row" style="display:none;"><span>{t.cart.loyaltyDiscount}</span><span id="sum-loyalty-discount">-0,00 €</span></div> <div class="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"> <div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label> <label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group"> <div class="coupon-input-group">
@@ -77,19 +78,19 @@ const t = useTranslations(lang);
</section> </section>
</Layout> </Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, freeLabel: t.checkout.free, cartLang: lang, mengenrabattTemplate: t.cart.mengenrabatt("__P__"), couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, buyNowLabel: t.cart.buyNow, keptInCartLabel: t.cart.keptInCart, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, 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. // 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>
<script type="module"> <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 { formatPrice } from "../../i18n/format";
import { getProduct, products } from "../../data/products"; import { getProduct, products } from "../../data/products";
import { berechneVersandkosten } from "../../data/versand"; import { berechneVersandkosten } from "../../data/versand";
import { effektiverZeilenpreis, mengenrabattProzent } from "../../data/rabatt"; import { mengenrabattProzent } from "../../data/rabatt";
import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../scripts/account"; 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"); const landSelect = document.getElementById("cart-land");
function render() { function render() {
@@ -108,7 +109,7 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../script
const zeilen = cart.map((i) => { const zeilen = cart.map((i) => {
const produkt = getProduct(i.slug); 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; const mengenProzent = produkt ? mengenrabattProzent(produkt, i.menge) : 0;
return { item: i, produkt, zeilenpreis, mengenProzent }; return { item: i, produkt, zeilenpreis, mengenProzent };
}); });
@@ -133,15 +134,17 @@ import { getAccount, treueFortschritt, setTreuebonusArtikel } from "../../script
const loyaltyControl = !treueAktiv ? "" : istTreueartikel const loyaltyControl = !treueAktiv ? "" : istTreueartikel
? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>` ? `<span class="loyalty-selected-badge">${loyaltySelectedLabel}</span>`
: `<label class="loyalty-select"><input type="radio" name="loyalty-artikel" data-loyalty-slug="${i.slug}" /> ${loyaltySelectLabel}</label>`; : `<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 ` return `
<div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}"> <div class="cart-item ${ausgewaehlt ? "" : "deselected"} ${istTreueartikel ? "loyalty-selected" : ""}">
<input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" /> <input type="checkbox" class="buy-toggle" data-slug="${i.slug}" ${ausgewaehlt ? "checked" : ""} aria-label="${buyNowLabel}" title="${buyNowLabel}" />
${thumb} ${thumb}
<div class="info"> <div class="info">
<strong>${i.name}</strong> <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} ${mengenBadge}
${keptBadge} ${keptBadge}
${gratisBadge}
${loyaltyControl} ${loyaltyControl}
</div> </div>
<div class="qty-controls"> <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) => { itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => { el.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
+246
View File
@@ -5,6 +5,17 @@
// Projekt-Vault) ersetzen — Struktur/Feldnamen bewusst schon so gewählt, dass sich eine echte // Projekt-Vault) ersetzen — Struktur/Feldnamen bewusst schon so gewählt, dass sich eine echte
// Session später 1:1 einsetzen lässt, ohne Aufrufstellen in der ganzen Seite anpassen zu müssen. // Session später 1:1 einsetzen lässt, ohne Aufrufstellen in der ganzen Seite anpassen zu müssen.
import {
type AboStufe,
aboStufenKonfig,
aboAktuellAktiv,
teddyMeilensteine,
naechsteMeilensteine,
addMonateISO,
heutigesDatumISO,
aboEinstellungen,
} from "../data/abo";
export interface Account { export interface Account {
loggedIn: boolean; loggedIn: boolean;
/** Echter Name (fest, z.B. für Bestellungen/Lieferadresse) — Pflichtangabe, unabhängig vom /** Echter Name (fest, z.B. für Bestellungen/Lieferadresse) — Pflichtangabe, unabhängig vom
@@ -31,6 +42,78 @@ export interface Account {
/** Slug des Artikels, den die Person im Warenkorb für die -20%-Belohnung ausgewählt hat /** Slug des Artikels, den die Person im Warenkorb für die -20%-Belohnung ausgewählt hat
* (siehe scripts/cart.ts / warenkorb-Seite). Wird bei Einlösung wieder geleert. */ * (siehe scripts/cart.ts / warenkorb-Seite). Wird bei Einlösung wieder geleert. */
treuebonusArtikelSlug: string | null; treuebonusArtikelSlug: string | null;
/** Monatliches Abo (siehe data/abo.ts für Preise/Regeln + weiter unten für die Aktionen). */
abo: AboZustand;
/** Kurze In-Konto-Benachrichtigungen (Phase 1: kein E-Mail-/Push-Versand, siehe Hinweis bei
* benachrichtigen() weiter unten) — neueste zuerst. */
benachrichtigungen: Benachrichtigung[];
}
export interface AboWechsel {
stufe: AboStufe;
von: string;
bis: string;
monate: number;
}
export interface AboZustand {
stufe: AboStufe | null;
startDatum: string | null;
/** Ende des aktuell bezahlten Abrechnungszeitraums — solange "heute" davor liegt, gilt das
* Abo als aktiv (siehe aboAktuellAktiv() in data/abo.ts), unabhängig vom Kündigungsstatus. */
bezahltBisDatum: string | null;
/** Durchgehend erfolgreich bezahlte Monate in der AKTUELLEN, ununterbrochenen Abo-Stufe —
* Grundlage für die Teddy-Meilensteine (siehe teddyMeilensteine() in data/abo.ts). */
bezahlteMonate: number;
gekuendigt: boolean;
gekuendigtAm: string | null;
/** Lifetime-Zähler, wachsen nur beim tatsächlichen Überschreiten eines Meilensteins (siehe
* aboZahlungSimulieren()) — überleben Stufenwechsel/Kündigung/Wiederanmeldung unverändert,
* damit bereits verdiente Prämien nie verloren gehen. */
kleinTeddyFreigeschaltetGesamt: number;
grossTeddyFreigeschaltetGesamt: number;
kleinTeddyEingeloest: number;
grossTeddyEingeloest: number;
historie: AboWechsel[];
}
/** Feste Schlüssel statt fertigem Text — der eigentliche, sprachabhängige Satz wird erst beim
* Anzeigen im Kundenkonto übersetzt (siehe aboLabels.notifKeyLabels in konto/angemeldet.astro).
* So bleiben Benachrichtigungen auch auf EN/CH/FR-Seiten korrekt in der jeweiligen Sprache,
* obwohl dieses Skript selbst sprachneutral ist (kein Zugriff auf useTranslations()). */
export type BenachrichtigungKey =
| "abo_abgeschlossen"
| "abo_gewechselt"
| "abo_zahlung_erfolgreich"
| "abo_teddy_klein_frei"
| "abo_teddy_gross_frei"
| "abo_bald_praemie"
| "abo_gekuendigt"
| "abo_teddy_klein_eingeloest"
| "abo_teddy_gross_eingeloest";
export interface Benachrichtigung {
key: BenachrichtigungKey;
/** Nur bei "abo_gewechselt" gesetzt — die neue Stufe, für den übersetzten Satz "{stufe}". */
stufe?: AboStufe;
datum: string;
gelesen: boolean;
}
function leeresAbo(): AboZustand {
return {
stufe: null,
startDatum: null,
bezahltBisDatum: null,
bezahlteMonate: 0,
gekuendigt: false,
gekuendigtAm: null,
kleinTeddyFreigeschaltetGesamt: 0,
grossTeddyFreigeschaltetGesamt: 0,
kleinTeddyEingeloest: 0,
grossTeddyEingeloest: 0,
historie: [],
};
} }
const KEY = "vandiy_account_v1"; const KEY = "vandiy_account_v1";
@@ -51,6 +134,8 @@ function leeresKonto(): Account {
treueBestellungen: 0, treueBestellungen: 0,
treuebonusAktiv: false, treuebonusAktiv: false,
treuebonusArtikelSlug: null, treuebonusArtikelSlug: null,
abo: leeresAbo(),
benachrichtigungen: [],
}; };
} }
@@ -191,3 +276,164 @@ export function treueFortschritt(a: Account): TreueFortschritt {
aktiv: a.treuebonusAktiv, aktiv: a.treuebonusAktiv,
}; };
} }
// ─── Abo-System (monatliche Mitgliedschaft mit dauerhaftem Bestellrabatt + Teddy-Prämien) ───
//
// Phase 1 (kein echtes Zahlungsbackend): aboZahlungSimulieren() steht für den monatlichen
// Abbuchungs-Webhook, den ein echter Zahlungsanbieter in Phase 2 auslösen würde — genau wie
// registriereAbgeschlosseneBestellung() oben schon für echte Bestell-Zahlungen steht. Nur
// Klicks auf diese Funktion zählen als "erfolgreich bezahlter Monat"; nichts hier läuft von
// selbst im Hintergrund, es gibt (noch) keinen Cronjob/Webhook, der das für echtes Geld täte.
//
// "Benachrichtigungen" sind hier bewusst als einfache In-Konto-Liste umgesetzt (siehe
// benachrichtigen()), NICHT als echte E-Mail/Push — das bräuchte einen Mailversand-Dienst, den
// es in Phase 1 nicht gibt. Deckt aber inhaltlich dieselben Ereignisse ab.
function benachrichtigen(a: Account, key: BenachrichtigungKey, stufe?: AboStufe): Account {
const eintrag: Benachrichtigung = { key, stufe, datum: heutigesDatumISO(), gelesen: false };
return { ...a, benachrichtigungen: [eintrag, ...a.benachrichtigungen].slice(0, 30) };
}
export function alleBenachrichtigungenGelesen() {
const a = getAccount();
saveAccount({ ...a, benachrichtigungen: a.benachrichtigungen.map((b) => ({ ...b, gelesen: true })) });
}
/** Abo neu abschließen ODER in eine andere Stufe wechseln. Bei einem Wechsel wird die bisherige
* Stufe mit ihren gesammelten Monaten in die Historie geschrieben (siehe Adminbereich-Regel
* `monateUeberStufenZusammenzaehlen`, ob die Monate in die neue Stufe übernommen werden). Bei
* einer Wiederanmeldung nach vollständig ausgelaufenem Abo greift stattdessen die Regel
* `fortsetzungBeiWiederanmeldung`. Verhindert doppelte Abbuchungen, indem eine laufende Stufe
* IMMER erst sauber beendet wird, bevor die neue beginnt. */
export function aboAbschliessen(stufe: AboStufe): Account {
let a = getAccount();
const heute = heutigesDatumISO();
const bisherStufe = a.abo.stufe;
const warNochAktivOderWartend = bisherStufe !== null;
const istWechsel = warNochAktivOderWartend && bisherStufe !== stufe;
const wiederanmeldungNachLapse = !warNochAktivOderWartend && a.abo.bezahlteMonate === 0 && a.abo.historie.length > 0;
let neueMonate = 0;
let historie = a.abo.historie;
if (warNochAktivOderWartend) {
// Laufende (oder gekündigt-aber-noch-bezahlte) Stufe sauber abschließen, BEVOR die neue
// beginnt — verhindert, dass beide gleichzeitig als "aktiv" gelten und doppelt abgebucht
// würde.
historie = [...historie, { stufe: bisherStufe!, von: a.abo.startDatum!, bis: heute, monate: a.abo.bezahlteMonate }];
if (istWechsel && aboEinstellungen.monateUeberStufenZusammenzaehlen) neueMonate = a.abo.bezahlteMonate;
} else if (wiederanmeldungNachLapse && aboEinstellungen.fortsetzungBeiWiederanmeldung) {
neueMonate = a.abo.historie[a.abo.historie.length - 1]?.monate ?? 0;
}
a = {
...a,
abo: {
...a.abo,
stufe,
startDatum: heute,
bezahltBisDatum: addMonateISO(heute, 1),
bezahlteMonate: neueMonate,
gekuendigt: false,
gekuendigtAm: null,
historie,
},
};
a = benachrichtigen(a, istWechsel ? "abo_gewechselt" : "abo_abgeschlossen", istWechsel ? stufe : undefined);
saveAccount(a);
return a;
}
/** Steht für die monatliche Abo-Abbuchung (siehe Hinweis oben). Verlängert den bezahlten
* Zeitraum um einen weiteren Monat, erhöht den Monatszähler und schaltet dabei automatisch
* neue Teddy-Meilensteine frei, sobald sie überschritten werden — jeder Meilenstein wird
* garantiert nur EIN einziges Mal gezählt (Differenz aus Vorher-/Nachher-Berechnung). Gekündigte
* Abos zahlen nicht weiter nach (ihre Vorteile laufen einfach zum bezahltBisDatum aus). */
export function aboZahlungSimulieren(): Account {
let a = getAccount();
if (!a.abo.stufe || a.abo.gekuendigt) return a;
const vorher = teddyMeilensteine(a.abo.stufe, a.abo.bezahlteMonate);
const neueMonate = a.abo.bezahlteMonate + 1;
const nachher = teddyMeilensteine(a.abo.stufe, neueMonate);
const basisDatum = a.abo.bezahltBisDatum && aboAktuellAktiv(a.abo.bezahltBisDatum) ? a.abo.bezahltBisDatum : heutigesDatumISO();
a = {
...a,
abo: {
...a.abo,
bezahlteMonate: neueMonate,
bezahltBisDatum: addMonateISO(basisDatum, 1),
kleinTeddyFreigeschaltetGesamt: a.abo.kleinTeddyFreigeschaltetGesamt + (nachher.klein - vorher.klein),
grossTeddyFreigeschaltetGesamt: a.abo.grossTeddyFreigeschaltetGesamt + (nachher.gross - vorher.gross),
},
};
a = benachrichtigen(a, "abo_zahlung_erfolgreich");
if (nachher.klein > vorher.klein) a = benachrichtigen(a, "abo_teddy_klein_frei");
if (nachher.gross > vorher.gross) a = benachrichtigen(a, "abo_teddy_gross_frei");
// Kurz VOR dem nächsten Meilenstein zusätzlich vorwarnen (ein Monat vorher).
const rest = naechsteMeilensteine(a.abo.stufe, neueMonate);
if (rest.klein === 1 || rest.gross === 1) a = benachrichtigen(a, "abo_bald_praemie");
saveAccount(a);
return a;
}
/** Kündigt zum Ende der bereits bezahlten Laufzeit — Vorteile (Rabatt, weitere
* Monatszählung/Meilensteine) laufen dann automatisch über aboAktuellAktiv()/aboZahlungSimulieren()
* aus, ohne dass hier sofort etwas "hart" abgeschaltet wird. */
export function aboKuendigen(): Account {
let a = getAccount();
if (!a.abo.stufe || a.abo.gekuendigt) return a;
a = { ...a, abo: { ...a.abo, gekuendigt: true, gekuendigtAm: heutigesDatumISO() } };
a = benachrichtigen(a, "abo_gekuendigt");
saveAccount(a);
return a;
}
/** Löst eine freigeschaltete, noch nicht eingelöste Teddy-Prämie ein — schreibt sie fest auf
* dieses Konto (Lifetime-Zähler steigt), damit sie nicht doppelt eingelöst werden kann. Das
* eigentliche Hinzufügen zum Warenkorb übernimmt die aufrufende Seite (siehe konto/angemeldet). */
export function teddyEinloesen(art: "klein" | "gross"): Account {
let a = getAccount();
const verfuegbar = art === "klein"
? a.abo.kleinTeddyFreigeschaltetGesamt - a.abo.kleinTeddyEingeloest
: a.abo.grossTeddyFreigeschaltetGesamt - a.abo.grossTeddyEingeloest;
if (verfuegbar <= 0) return a;
a = art === "klein"
? { ...a, abo: { ...a.abo, kleinTeddyEingeloest: a.abo.kleinTeddyEingeloest + 1 } }
: { ...a, abo: { ...a.abo, grossTeddyEingeloest: a.abo.grossTeddyEingeloest + 1 } };
a = benachrichtigen(a, art === "klein" ? "abo_teddy_klein_eingeloest" : "abo_teddy_gross_eingeloest");
saveAccount(a);
return a;
}
export interface AboStatus {
stufe: AboStufe | null;
aktiv: boolean;
rabattProzent: number;
preis: number;
bezahlteMonate: number;
naechsterKleinIn: number | null;
naechsterGrossIn: number | null;
kleinVerfuegbar: number;
grossVerfuegbar: number;
}
/** Aufbereiteter Gesamtstatus fürs Kundenkonto — bündelt Aktiv-Check, aktuellen Rabattsatz und
* Prämien-Fortschritt an einer Stelle, damit UI-Code nicht selbst rechnen muss. */
export function aboStatus(a: Account): AboStatus {
const aktiv = aboAktuellAktiv(a.abo.bezahltBisDatum);
const stufe = a.abo.stufe;
const konfig = stufe ? aboStufenKonfig(stufe) : null;
const rest = stufe ? naechsteMeilensteine(stufe, a.abo.bezahlteMonate) : { klein: null, gross: null };
return {
stufe,
aktiv,
rabattProzent: aktiv && konfig ? konfig.rabattProzent : 0,
preis: konfig?.preis ?? 0,
bezahlteMonate: a.abo.bezahlteMonate,
naechsterKleinIn: rest.klein,
naechsterGrossIn: rest.gross,
kleinVerfuegbar: Math.max(0, a.abo.kleinTeddyFreigeschaltetGesamt - a.abo.kleinTeddyEingeloest),
grossVerfuegbar: Math.max(0, a.abo.grossTeddyFreigeschaltetGesamt - a.abo.grossTeddyEingeloest),
};
}
+54 -10
View File
@@ -5,7 +5,8 @@ import { getProduct } from "../data/products";
import { effektiverPreis, effektiverZeilenpreis, istImSale } from "../data/rabatt"; import { effektiverPreis, effektiverZeilenpreis, istImSale } from "../data/rabatt";
import { findeGutschein, gutscheinRabatt, type Gutschein } from "../data/gutscheine"; import { findeGutschein, gutscheinRabatt, type Gutschein } from "../data/gutscheine";
import { findePartnercode, partnercodeRabatt, type Partnercode } from "../data/partnercodes"; import { findePartnercode, partnercodeRabatt, type Partnercode } from "../data/partnercodes";
import { getAccount, TREUE_RABATT_PROZENT } from "./account"; import { getAccount, TREUE_RABATT_PROZENT, aboStatus } from "./account";
import { aboEinstellungen } from "../data/abo";
export interface CartItem { export interface CartItem {
slug: string; slug: string;
@@ -16,6 +17,10 @@ export interface CartItem {
// später. Fehlt das Feld (ältere, bereits gespeicherte Warenkörbe), gilt es als ausgewählt — // später. Fehlt das Feld (ältere, bereits gespeicherte Warenkörbe), gilt es als ausgewählt —
// siehe istAusgewaehlt(). // siehe istAusgewaehlt().
ausgewaehlt?: boolean; ausgewaehlt?: boolean;
/** true bei einer eingelösten Teddy-Prämie aus dem Abo-System (siehe fuegeTeddyGratisHinzu())
* — kostet 0 €, unabhängig vom eigentlichen Produktpreis (der bleibt für die Anzeige "regulär
* X €, für dich gratis" erhalten). */
gratis?: boolean;
} }
/** true, sofern nicht explizit auf false gesetzt — hält ältere localStorage-Warenkörbe ohne /** true, sofern nicht explizit auf false gesetzt — hält ältere localStorage-Warenkörbe ohne
@@ -24,6 +29,15 @@ export function istAusgewaehlt(item: CartItem): boolean {
return item.ausgewaehlt !== false; return item.ausgewaehlt !== false;
} }
/** Zeilenpreis EINER Warenkorb-Position — 0 €, wenn es sich um eine eingelöste Gratis-Prämie
* handelt, sonst der normale (rabattierte) Zeilenpreis. Zentrale Stelle, damit Warenkorb-Liste,
* Zwischensumme UND Checkout-Beleg garantiert denselben Betrag zeigen. */
export function zeilenpreisFuer(item: CartItem): number {
if (item.gratis) return 0;
const produkt = getProduct(item.slug);
return produkt ? effektiverZeilenpreis(produkt, item.menge) : item.menge * item.preis;
}
const KEY = "vandiy_cart_v1"; const KEY = "vandiy_cart_v1";
export function getCart(): CartItem[] { export function getCart(): CartItem[] {
@@ -97,10 +111,24 @@ export function cartCount(): number {
export function cartTotal(): number { export function cartTotal(): number {
return getCart() return getCart()
.filter(istAusgewaehlt) .filter(istAusgewaehlt)
.reduce((sum, i) => { .reduce((sum, i) => sum + zeilenpreisFuer(i), 0);
const produkt = getProduct(i.slug); }
return sum + (produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis);
}, 0); /** Fügt eine eingelöste Teddy-Prämie (siehe scripts/account.ts → teddyEinloesen()) als echten,
* aber kostenlosen Artikel zum Warenkorb hinzu — taucht ganz normal in der Liste auf, zählt
* aber mit 0 € in die Summe (siehe zeilenpreisFuer()). */
export function fuegeTeddyGratisHinzu(art: "klein" | "gross") {
const slug = art === "klein" ? "kleiner-teddy" : "grosser-teddy";
const produkt = getProduct(slug);
const name = produkt?.name.de ?? (art === "klein" ? "Kleiner Teddy" : "Großer Teddy");
const cart = getCart();
const existing = cart.find((i) => i.slug === slug && i.gratis);
if (existing) {
existing.menge += 1;
} else {
cart.push({ slug, name, preis: produkt?.preis ?? 0, menge: 1, ausgewaehlt: true, gratis: true });
}
saveCart(cart);
} }
const COUPON_KEY = "vandiy_coupon_v1"; const COUPON_KEY = "vandiy_coupon_v1";
@@ -206,9 +234,25 @@ export function treuebonusDiscount(): number {
return einzelpreis * (TREUE_RABATT_PROZENT / 100); return einzelpreis * (TREUE_RABATT_PROZENT / 100);
} }
/** Gutschein, Partnercode und Treuebonus können gleichzeitig aktiv sein — die Summe wird hier // Abo-Rabatt (siehe scripts/account.ts + data/abo.ts): gilt automatisch auf JEDE Bestellung,
* auf die Zwischensumme gedeckelt, damit der Gesamtpreis nie unter 0 € rutscht, egal wie // solange ein Abo aktiv und bezahlt ist — kein Code nötig. Ob er sich mit Gutschein/Partnercode/
* großzügig die einzelnen Rabatte zusammen sind. */ // Treuebonus kombinieren lässt, legt VanVan im Adminbereich fest (kombinierbarMitAnderenRabatten).
export function totalDiscount(subtotal: number): number {
return Math.min(subtotal, couponDiscount(subtotal) + partnerCodeDiscount(subtotal) + treuebonusDiscount()); /** Abo-Rabatt in Euro auf die komplette Zwischensumme (0, wenn kein Abo aktiv ist). */
export function aboDiscount(subtotal: number): number {
const { aktiv, rabattProzent } = aboStatus(getAccount());
return aktiv && rabattProzent > 0 ? subtotal * (rabattProzent / 100) : 0;
}
/** Gutschein, Partnercode, Treuebonus und Abo-Rabatt können grundsätzlich gleichzeitig aktiv
* sein. Ist der Abo-Rabatt NICHT mit anderen Rabatten kombinierbar (Adminbereich-Einstellung),
* gilt bei aktivem Abo ausschließlich dieser — alle anderen werden für diese Bestellung
* ignoriert. Die Summe wird immer auf die Zwischensumme gedeckelt, damit der Gesamtpreis nie
* unter 0 € rutscht. */
export function totalDiscount(subtotal: number): number {
const abo = aboDiscount(subtotal);
if (abo > 0 && !aboEinstellungen.kombinierbarMitAnderenRabatten) {
return Math.min(subtotal, abo);
}
return Math.min(subtotal, couponDiscount(subtotal) + partnerCodeDiscount(subtotal) + treuebonusDiscount() + abo);
} }
+90 -1
View File
@@ -1344,7 +1344,7 @@ a:focus-visible, button:focus-visible {
} }
.account-head > * { position: relative; z-index: 1; } .account-head > * { position: relative; z-index: 1; }
.account-head-identity { display: flex; align-items: center; gap: 1rem; } .account-head-identity { display: flex; align-items: center; gap: 1rem; }
.account-head > #dash-logout-btn { margin-left: auto; } .account-head-actions { margin-left: auto; display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; }
.account-head h1 { margin: 0.2rem 0 0.5rem; } .account-head h1 { margin: 0.2rem 0 0.5rem; }
.account-head .lead { margin: 0; max-width: 52ch; } .account-head .lead { margin: 0; max-width: 52ch; }
.account-head-sparkle { .account-head-sparkle {
@@ -1482,6 +1482,9 @@ a:focus-visible, button:focus-visible {
keinen Fall übersehen werden, wenn die Belohnung bereit ist. */ keinen Fall übersehen werden, wenn die Belohnung bereit ist. */
.account-nav-loyalty.is-ready { animation-duration: 1.4s, 1.1s; } .account-nav-loyalty.is-ready { animation-duration: 1.4s, 1.1s; }
.account-nav-loyalty.is-ready::after { height: 4px; } .account-nav-loyalty.is-ready::after { height: 4px; }
.account-nav-abo { --nav-color: #e3b23c; --nav-delay: 2s; }
.account-nav-abo.is-ready { animation-duration: 1.4s, 1.1s; }
.account-nav-abo.is-ready::after { height: 4px; }
/* Treuebonus-Fortschrittskarte im Inhaltsbereich — eigener grüner Farbton, sonst dieselbe /* Treuebonus-Fortschrittskarte im Inhaltsbereich — eigener grüner Farbton, sonst dieselbe
"besondere" Formsprache wie die anderen Konto-Karten (siehe .profile-card). */ "besondere" Formsprache wie die anderen Konto-Karten (siehe .profile-card). */
@@ -2347,3 +2350,89 @@ a:focus-visible, button:focus-visible {
/* Konto */ /* Konto */
.account-grid { align-items: start; } .account-grid { align-items: start; }
/* "Mein Abo verwalten"-Button neben Abmelden: bewusst deutlich auffälliger als der normale
btn-outline-Rahmen der übrigen Kopfzeile — eigener Gold/Lila-Farbverlauf, wanderndes Glanzlicht
und ein leicht pulsierender Schein, damit der Einstieg ins neue Abosystem sofort ins Auge
fällt (explizit vom Kunden gewünscht: "ein mega geiler Button ... neben dem Abmelden"). */
.btn-abo-hero {
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.7rem 1.3rem;
border-radius: var(--radius-l);
border: 1.5px solid transparent;
background:
linear-gradient(155deg, var(--c-anthracite-light), var(--c-anthracite)) padding-box,
linear-gradient(120deg, #e3b23c, #9b7fd6, var(--c-accent), #e3b23c) border-box;
background-size: 100% 100%, 300% 100%;
color: #f9e9b8;
font-weight: 800;
font-size: 0.95rem;
letter-spacing: 0.01em;
cursor: pointer;
animation: abo-hero-border-shift 5s ease-in-out infinite, abo-hero-glow 2.4s ease-in-out infinite;
transition: transform 0.2s var(--ease);
}
.btn-abo-hero:hover { transform: translateY(-2px) scale(1.03); }
.btn-abo-hero::after {
content: "";
position: absolute;
top: 0;
left: -60%;
width: 35%;
height: 100%;
background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.35), transparent);
transform: skewX(-18deg);
animation: account-head-shine 3.4s ease-in-out infinite;
pointer-events: none;
}
.btn-abo-hero-sparkle { display: inline-block; animation: account-nav-sparkle-twinkle 2.2s ease-in-out infinite; }
@keyframes abo-hero-border-shift {
0%, 100% { background-position: 0 0, 0% 50%; }
50% { background-position: 0 0, 100% 50%; }
}
@keyframes abo-hero-glow {
0%, 100% { box-shadow: 0 0 0 2px color-mix(in srgb, #e3b23c 30%, transparent), 0 0 16px -4px color-mix(in srgb, #e3b23c 60%, transparent); }
50% { box-shadow: 0 0 0 2px color-mix(in srgb, #9b7fd6 40%, transparent), 0 0 24px -2px color-mix(in srgb, #9b7fd6 70%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .btn-abo-hero, .btn-abo-hero::after, .btn-abo-hero-sparkle { animation: none; } }
/* Abosystem: drei Planvergleichs-Karten + Statuskarte + Benachrichtigungsliste. */
.abo-plans { margin: 1.4rem 0 1.6rem; }
.abo-plan-card {
position: relative;
display: flex;
flex-direction: column;
gap: 0.6rem;
border: 1.5px solid rgba(244, 241, 247, 0.14);
transition: border-color 0.2s var(--ease), transform 0.2s var(--ease);
}
.abo-plan-card:hover { transform: translateY(-3px); }
.abo-plan-klein { --plan-color: #6ecb8f; }
.abo-plan-mittel { --plan-color: var(--c-accent); }
.abo-plan-gross { --plan-color: #e3b23c; }
.abo-plan-card h3 { margin: 0.2rem 0 0; color: var(--plan-color, var(--c-accent)); }
.abo-plan-price { font-family: var(--font-head); font-size: 1.6rem; font-weight: 700; margin: 0; }
.abo-plan-vorteil { margin: 0; color: var(--c-text-muted); flex-grow: 1; }
.abo-plan-card.is-current-plan { border-color: var(--plan-color, var(--c-accent)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--plan-color, var(--c-accent)) 50%, transparent), 0 0 20px -8px color-mix(in srgb, var(--plan-color, var(--c-accent)) 60%, transparent); }
.abo-plan-badge { align-self: flex-start; background: color-mix(in srgb, var(--plan-color, var(--c-accent)) 30%, transparent); border: 1px solid color-mix(in srgb, var(--plan-color, var(--c-accent)) 55%, transparent); border-radius: 999px; padding: 0.2rem 0.7rem; font-size: 0.78rem; font-weight: 700; }
.abo-plan-btn { align-self: flex-start; }
.abo-plan-btn:disabled { opacity: 0.6; cursor: default; }
.abo-status-card { margin-bottom: 1.4rem; }
.abo-status-card h3 { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.abo-badge-active { background: rgba(126, 200, 160, 0.28); border-color: rgba(126, 200, 160, 0.5); color: #cfeee0; }
.abo-badge-inactive { background: color-mix(in srgb, var(--c-sale) 30%, transparent); border-color: color-mix(in srgb, var(--c-sale) 55%, transparent); }
.abo-praemien-block { border-top: 1px dashed rgba(244, 241, 247, 0.14); padding-top: 0.9rem; margin-top: 0.3rem; }
.abo-praemien-block h4 { margin: 0 0 0.4rem; }
.abo-praemien-row { display: flex; gap: 0.7rem; flex-wrap: wrap; }
.abo-actions { display: flex; gap: 0.7rem; flex-wrap: wrap; margin-top: 0.6rem; }
.abo-cancel-btn { border-color: color-mix(in srgb, var(--c-sale) 55%, transparent); color: var(--c-sale); }
.abo-notif-list { list-style: none; margin: 0.6rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; max-height: 260px; overflow-y: auto; }
.abo-notif-item { padding: 0.5rem 0.7rem; border-radius: var(--radius-m); background: rgba(244, 241, 247, 0.05); border-left: 3px solid transparent; }
.abo-notif-item.abo-notif-unread { border-left-color: var(--c-accent); background: color-mix(in srgb, var(--c-accent) 10%, transparent); }
@media (prefers-reduced-motion: reduce) { .abo-plan-card { transition: none; } }