Files
vans-diy-bastelbedarf/src/pages/en/konto/angemeldet.astro
T
qciganoandClaude Sonnet 5 bf0d169015 Echtes Google-/PayPal-Login: OAuth 2.0 + PKCE, eigene Kundendatenbank, DSGVO-Selbstbedienung
Kundenkonten sind jetzt genauso echt wie die PayPal-Zahlung: server-geprüftes OAuth 2.0 mit
PKCE für Google und "Log in with PayPal" (functions/_shared/oauth.js, oauth-handlers.js),
neue D1-Tabelle "customers" (bewusst ohne Passwort-Feld), eigene von der Zugangscode-Schranke
getrennte Sitzungs-Logik (customer-auth.js). Echte DSGVO-Rechte direkt im Kontobereich:
Daten herunterladen (Art. 15/20) und Konto unwiderruflich löschen (Art. 17), Bestellungen
bleiben aus gesetzlichen Gründen erhalten. Datenschutzerklärung entsprechend ergänzt.

Ohne echte Google-/PayPal-Zugangsdaten zeigt der Login-Button ehrlich einen
"noch nicht eingerichtet"-Hinweis statt eine Anmeldung vorzutäuschen.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-04 01:45:55 +02:00

1045 lines
56 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Layout from "../../../layouts/Layout.astro";
import ProductCard from "../../../components/ProductCard.astro";
import { products, getProduct } from "../../../data/products";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format";
import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, type AboStufe } from "../../../data/abo";
import { TREUE_ZIEL, TREUE_RABATT_PROZENT } from "../../../scripts/account";
const lang: Locale = "en";
const t = useTranslations(lang);
// Bestellungen sind weiterhin Beispieldaten (siehe previewNote) — ECHT sind ab jetzt aber
// Anmeldung, Profil (Benutzername/Foto) und Rezensionen, gespeichert im localStorage dieses
// Geräts (siehe scripts/account.ts, scripts/reviews.ts). Jede Bestellposition referenziert
// einen echten Produkt-Slug — der Name kommt live aus den Produktdaten (mehrsprachig, immer
// konsistent mit dem Shop), damit "Rezension schreiben" wirklich zur passenden Seite führt.
const beispielBestellungenRoh = [
{
nummer: "VDB-2026-0184",
datum: "28.07.2026",
status: "versendet" as const,
tracking: "00340434161094015791",
artikel: [
{ slug: "hasidog-plushie-blaueroverall", menge: 1 },
{ slug: "perlen-armband-hellblau", menge: 1 },
],
},
{
nummer: "VDB-2026-0157",
datum: "12.07.2026",
status: "abgeschlossen" as const,
tracking: null,
artikel: [{ slug: "haekelnadel-set-ergonomisch", menge: 1 }],
},
];
const beispielBestellungen = beispielBestellungenRoh.map((b) => {
const artikel = b.artikel.map((a) => {
const p = getProduct(a.slug);
return { ...a, name: p?.name[lang] ?? a.slug, preis: p?.preis ?? 0 };
});
return { ...b, artikel, summe: artikel.reduce((s, a) => s + a.preis * a.menge, 0) };
});
const statusLabel = {
bezahlt: t.accountDash.statusBezahlt,
bearbeitung: t.accountDash.statusBearbeitung,
versandVorbereitet: t.accountDash.statusVersandVorbereitet,
versendet: t.accountDash.statusVersendet,
abgeschlossen: t.accountDash.statusAbgeschlossen,
} as const;
// Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren
// Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß.
const aboPlanLabels: Record<AboStufe, { name: string; vorteil: string }> = {
klein: { name: aboStufeName("klein", lang), vorteil: aboStufeVorteil("klein", lang) },
mittel: { name: aboStufeName("mittel", lang), vorteil: aboStufeVorteil("mittel", lang) },
gross: { name: aboStufeName("gross", lang), vorteil: aboStufeVorteil("gross", lang) },
};
// Eigenes Icon je Stufe (rein optisch, nicht admin-editierbar) — soll auf einen Blick erkennbar
// machen, dass es sich um eine aufsteigende Reihe handelt (Keimling → Blatt → Krone).
const aboPlanIcon: Record<AboStufe, string> = { klein: "🌱", mittel: "🌿", gross: "👑" };
// "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop —
// in Phase 2 kommt hier die tatsächliche Verlaufs-Historie der angemeldeten Person hin.
const zuletztAngesehen = products.slice(0, 3);
// "Offene Sendungen" = alles außer abgeschlossen (bezahlt/bearbeitung/versandVorbereitet/
// versendet), damit die Zahl exakt zu dem passt, was der "Offene Sendungen"-Tab tatsächlich
// zeigt. "Bestellungen" zählt entsprechend nur die schon angekommenen (abgeschlossenen).
const offeneSendungen = beispielBestellungen.filter((b) => b.status !== "abgeschlossen").length;
const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === "abgeschlossen").length;
---
<Layout title="My Account" description="Personal customer area at Van's DIY & Bastelbedarf." lang={lang} path="/konto/angemeldet/">
<section class="section-tight">
<div class="container">
<p class="todo-note" id="not-logged-in-note" style="display:none;">{t.accountDash.notLoggedIn}</p>
<p class="todo-note" id="preview-note">{t.accountDash.previewNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container">
<div class="account-head card">
<span class="account-head-sparkle account-head-sparkle-1" aria-hidden="true">✨</span>
<span class="account-head-sparkle account-head-sparkle-2" aria-hidden="true">✨</span>
<div class="account-head-identity">
<span class="account-avatar-lg" id="dash-avatar" aria-hidden="true"></span>
<div>
<span class="eyebrow">{t.accountDash.eyebrow}</span>
<h1 id="dash-greeting">{t.accountDash.greeting("…")}</h1>
<p class="lead">{t.accountDash.lead}</p>
</div>
</div>
<div class="account-head-actions">
<button type="button" class="btn btn-outline btn-sm" id="dash-logout-btn">{t.accountDash.logout}</button>
</div>
<div class="account-head-daten-row">
<button type="button" class="btn btn-daten-hero" id="dash-daten-btn">
<span class="btn-daten-hero-sparkle" aria-hidden="true">✨</span>
{t.accountDash.datenHeaderBtn}
</button>
</div>
</div>
{/* Schnellnavigation: eine zusammenhängende Leiste direkt über "Meine Bestellungen" /
"Meine Wunschliste" statt drei loser Kästchen — jedes Segment springt zum passenden
Abschnitt weiter unten, Icon und Akzentfarbe passen zur jeweiligen Überschrift. */}
<nav class="account-nav" aria-label={t.accountDash.navLabel}>
<button type="button" class="account-nav-item account-nav-orders" data-tab="bestellungen">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">📦</span>
<span class="account-nav-value">{angekommeneBestellungen}</span>
<span class="account-nav-label">{t.accountDash.statOrders}</span>
</button>
<button type="button" class="account-nav-item account-nav-open" data-tab="offene">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🚚</span>
<span class="account-nav-value">{offeneSendungen}</span>
<span class="account-nav-label">{t.accountDash.statOpen}</span>
</button>
<button type="button" class="account-nav-item account-nav-wishlist" data-tab="wunschliste">
<span class="account-nav-sparkle" aria-hidden="true">✨</span>
<span class="account-nav-icon" aria-hidden="true">🩵</span>
<span class="account-nav-value">0</span>
<span class="account-nav-label">{t.accountDash.statWishlist}</span>
</button>
<button type="button" class="account-nav-item account-nav-loyalty" data-tab="treuebonus">
<span class="account-nav-sparkle" aria-hidden="true">🌟</span>
<span class="account-nav-icon" aria-hidden="true">🎁</span>
<span class="account-nav-value" id="loyalty-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statLoyalty}</span>
</button>
<button type="button" class="account-nav-item account-nav-abo" data-tab="abo">
<span class="account-nav-sparkle" aria-hidden="true">🌟</span>
<span class="account-nav-icon" aria-hidden="true">🎫</span>
<span class="account-nav-value" id="abo-nav-value"></span>
<span class="account-nav-label">{t.accountDash.statAbo}</span>
</button>
</nav>
</div>
</section>
<section class="section-tight" id="bestellungen">
<div class="container">
<h2>{t.accountDash.ordersTitle}</h2>
<p class="small" id="orders-empty-note" style="display:none;" data-empty-open={t.accountDash.noOpenOrders} data-empty-arrived={t.accountDash.noArrivedOrders}></p>
<div class="order-list" id="order-list">
{beispielBestellungen.map((b) => (
<div class={`card order-card order-card-${b.status}`}>
<div class="order-card-head">
<div>
<span class="order-no">{t.accountDash.orderNo} {b.nummer}</span>
<span class="small order-meta">{t.accountDash.orderDate} {b.datum}</span>
</div>
<span class={`badge order-status order-status-${b.status}`}>{statusLabel[b.status]}</span>
</div>
<ul class="order-items">
{b.artikel.map((a) => (
<li>
<span>{a.menge}× {a.name}</span>
<span class="order-item-right">
<span class="order-item-price">{formatPrice(a.preis * a.menge, lang)}</span>
{/* Zustand (Button vs. "✓ Bewertet") hängt vom localStorage ab — wird per JS
beim Laden gesetzt (siehe Script unten), Grundzustand ist der Button. */}
<span class="order-item-review" data-order-review data-bestellnummer={b.nummer} data-slug={a.slug}>
<a class="small review-link" href={`/en/produkt/${a.slug}/?bewerten=${b.nummer}`}>{t.accountDash.reviewBtn}</a>
</span>
</span>
</li>
))}
</ul>
<div class="order-card-foot">
<span class="order-total">{t.accountDash.orderTotal}: <strong>{formatPrice(b.summe, lang)}</strong></span>
<div class="order-actions">
{b.tracking && <a class="btn btn-outline btn-sm" href={`https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=${b.tracking}`} target="_blank" rel="noopener">{t.accountDash.trackingBtn}</a>}
<button type="button" class="btn btn-outline btn-sm" data-open-invoice={`invoice-${b.nummer}`}>{t.accountDash.invoiceBtn}</button>
<button
type="button"
class="btn btn-outline btn-sm"
data-download-invoice={b.nummer}
data-invoice-datum={b.datum}
data-invoice-summe={b.summe}
data-invoice-artikel={JSON.stringify(b.artikel.map((a) => ({ name: a.name, menge: a.menge, preis: a.preis })))}
data-label-default={t.accountDash.invoiceDownloadBtn}
data-label-loading={t.accountDash.invoiceDownloading}
>{t.accountDash.invoiceDownloadBtn}</button>
<a class="btn btn-outline btn-sm" href="/en/shop/">{t.accountDash.reorderBtn}</a>
</div>
</div>
{b.tracking && (
<p class="small order-tracking-no">{t.accountDash.trackingLabel}: <code>{b.tracking}</code></p>
)}
{/* Rechnung direkt auf der Seite ansehen (natives <dialog>, siehe Skript unten) —
kein Download nötig. Empfängername wird beim Öffnen live aus dem Konto befüllt
(siehe .invoice-billto-name), Adresse ist bewusst dieselbe Platzhalter-Adresse
wie oben unter "Meine Daten". */}
<dialog class="invoice-dialog" id={`invoice-${b.nummer}`}>
<div class="invoice-card">
<div class="invoice-head">
<div>
<h3>{t.accountDash.invoiceTitle}</h3>
<p class="invoice-meta">{t.accountDash.invoiceNumberLabel}: {b.nummer}</p>
<p class="invoice-meta">{t.accountDash.invoiceDateLabel}: {b.datum}</p>
</div>
<button type="button" class="invoice-close" data-close-invoice aria-label={t.accountDash.invoiceCloseBtn}>✕</button>
</div>
<div class="invoice-parties">
<div>
<h4>{t.accountDash.invoiceSellerLabel}</h4>
<p>Van's DIY &amp; Bastelbedarf</p>
</div>
<div>
<h4>{t.accountDash.invoiceBillToLabel}</h4>
<p><span class="invoice-billto-name"></span><br />Musterstraße 1<br />10115 Berlin<br />Deutschland</p>
</div>
</div>
<table class="invoice-table">
<thead>
<tr>
<th>{t.accountDash.invoiceItemLabel}</th>
<th>{t.accountDash.invoiceQtyLabel}</th>
<th>{t.accountDash.invoiceUnitPriceLabel}</th>
<th>{t.accountDash.invoiceSumLabel}</th>
</tr>
</thead>
<tbody>
{b.artikel.map((a) => (
<tr>
<td>{a.name}</td>
<td>{a.menge}</td>
<td>{formatPrice(a.preis, lang)}</td>
<td>{formatPrice(a.preis * a.menge, lang)}</td>
</tr>
))}
<tr class="invoice-total-row">
<td colspan="3">{t.accountDash.orderTotal}</td>
<td>{formatPrice(b.summe, lang)}</td>
</tr>
</tbody>
</table>
<p class="invoice-vat-note">{t.common.vatNote}</p>
<div class="invoice-foot">
<button type="button" class="btn btn-outline btn-sm" data-close-invoice>{t.accountDash.invoiceCloseBtn}</button>
</div>
</div>
</dialog>
</div>
))}
</div>
</div>
</section>
<section class="section-tight" id="wunschliste">
<div class="container">
<h2>{t.accountDash.wishlistTitle}</h2>
<div class="card">
<p class="small" style="margin:0;">{t.accountDash.wishlistEmpty}</p>
</div>
</div>
</section>
{zuletztAngesehen.length > 0 && (
<section class="section-tight" id="zuletzt-angesehen">
<div class="container">
<h2>{t.accountDash.recentTitle}</h2>
<div class="grid grid-3">{zuletztAngesehen.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
<section class="section-tight" id="meine-daten">
<div class="container">
<h2>{t.accountDash.profileTitle}</h2>
<div class="card profile-card">
<span class="profile-card-sparkle profile-card-sparkle-1" aria-hidden="true">✨</span>
<span class="profile-card-sparkle profile-card-sparkle-2" aria-hidden="true">✨</span>
<div class="profile-photo-row">
<span class="account-avatar-lg" id="profile-avatar" aria-hidden="true"></span>
<div>
<span class="small" style="display:block; margin-bottom:0.4rem;">{t.accountDash.photoLabel}</span>
<label class="btn btn-outline btn-sm profile-photo-btn">
{t.accountDash.photoChange}
<input type="file" accept="image/*" id="profile-photo-input" style="display:none;" />
</label>
<button type="button" class="btn btn-outline btn-sm" id="profile-photo-remove">{t.accountDash.photoRemove}</button>
</div>
</div>
{/* Optionaler Spitzname zuerst (Bonus, wird bevorzugt angezeigt), darunter der feste
Pflicht-Name (z.B. für Bestellungen/Lieferadresse). */}
<div class="profile-field">
<label for="profile-username">{t.accountDash.usernameLabel}</label>
<div class="profile-field-row">
<input type="text" id="profile-username" maxlength="40" />
<button type="button" class="btn btn-primary btn-sm" id="profile-username-save">{t.accountDash.usernameSave}</button>
</div>
<span class="small" id="profile-username-status" style="display:none; color: var(--c-accent);">{t.accountDash.usernameSaved}</span>
<span class="small" style="display:block; color: var(--c-text-muted); margin-top:0.3em;">{t.accountDash.usernameHint}</span>
</div>
<table class="specs">
<tbody>
{/* Name, E-Mail und Lieferadresse sind fest (wie bei einer echten Bestellung) — nur
der Spitzname oben ist änderbar. */}
<tr><th>{t.accountDash.profileName}</th><td id="profile-name-display"></td></tr>
<tr><th>{t.accountDash.profileEmail}</th><td id="profile-email"></td></tr>
<tr><th>{t.accountDash.profileAddress}</th><td>Musterstraße 1<br />10115 Berlin<br />Deutschland</td></tr>
</tbody>
</table>
{/* Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20) — nur relevant/sichtbar, wenn
eine echte Google-/PayPal-Sitzung aktiv ist (siehe versucheEchteSessionZuUebernehmen()
im Skript unten). Bei einer reinen Demo-/Vorschau-Sitzung ohne Server-Konto gibt es
nichts zu exportieren/löschen, daher standardmäßig ausgeblendet. */}
<div class="profile-dsgvo-actions" id="profile-dsgvo-actions" style="display:none;">
<a class="btn btn-outline btn-sm" href="/api/account/export" id="dsgvo-export-btn">{t.accountDash.exportDataButton}</a>
<button type="button" class="btn btn-outline btn-sm" id="dsgvo-delete-btn">{t.accountDash.deleteAccountButton}</button>
<p class="small" id="dsgvo-delete-error" style="display:none; color: var(--c-sale);">{t.accountDash.deleteAccountError}</p>
</div>
</div>
</div>
</section>
{/* Treuebonus: 10 gültige, abgeschlossene Bestellungen → 20% auf einen frei wählbaren Artikel
(siehe scripts/account.ts). Fortschritt/Status kommen komplett aus dem Konto und werden per
JS befüllt (siehe Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="treuebonus">
<div class="container">
<h2>{t.accountDash.loyaltyTitle}</h2>
<div class="card loyalty-card">
<span class="loyalty-card-sparkle loyalty-card-sparkle-1" aria-hidden="true">✨</span>
<span class="loyalty-card-sparkle loyalty-card-sparkle-2" aria-hidden="true">✨</span>
<div class="loyalty-inner" id="loyalty-locked-content" style="display:none;">
<p class="loyalty-locked-title">{t.accountDash.loyaltyLockedTitle}</p>
<p class="small">{t.accountDash.loyaltyLockedText}</p>
<button type="button" class="btn btn-primary btn-sm" id="loyalty-locked-abo-btn" style="align-self:flex-start;">{t.accountDash.loyaltyLockedBtn}</button>
</div>
<div class="loyalty-inner" id="loyalty-unlocked-content">
<div class="loyalty-progress-track">
<div class="loyalty-progress-bar">
<div class="loyalty-progress-fill" id="loyalty-bar-fill"></div>
<div class="loyalty-progress-dots" aria-hidden="true">
{Array.from({ length: TREUE_ZIEL - 1 }).map((_, i) => (
<span class="loyalty-progress-dot" style={`left:${((i + 1) / TREUE_ZIEL) * 100}%`}></span>
))}
</div>
</div>
<span class="loyalty-progress-goal-icon" aria-hidden="true">🎁</span>
</div>
<p class="loyalty-progress-text" id="loyalty-progress-text"></p>
<div class="abo-stat-grid loyalty-stat-grid">
<div class="abo-stat-tile">
<span class="abo-stat-icon" aria-hidden="true">📦</span>
<span class="abo-stat-label">{t.accountDash.loyaltyStatCollected}</span>
<span class="abo-stat-value" id="loyalty-stat-collected"></span>
</div>
<div class="abo-stat-tile">
<span class="abo-stat-icon" aria-hidden="true">⏳</span>
<span class="abo-stat-label">{t.accountDash.loyaltyStatMissing}</span>
<span class="abo-stat-value" id="loyalty-stat-missing"></span>
</div>
<div class="abo-stat-tile loyalty-stat-tile-highlight">
<span class="abo-stat-icon" aria-hidden="true">💰</span>
<span class="abo-stat-label">{t.accountDash.loyaltyStatBonus}</span>
<span class="abo-stat-value">{TREUE_RABATT_PROZENT}%</span>
</div>
<div class="abo-stat-tile">
<span class="abo-stat-icon" aria-hidden="true">🏅</span>
<span class="abo-stat-label">{t.accountDash.loyaltyStatRedeemed}</span>
<span class="abo-stat-value" id="loyalty-stat-redeemed"></span>
</div>
</div>
<p class="small loyalty-last-redeemed" id="loyalty-last-redeemed" style="display:none;"></p>
<div class="loyalty-ready-block" id="loyalty-ready-block" style="display:none;">
<span class="loyalty-ready-icon" aria-hidden="true">🎁</span>
<div class="loyalty-ready-body">
<strong>{t.accountDash.loyaltyUnlockedTitle}</strong>
<p class="small">{t.accountDash.loyaltyUnlockedText}</p>
<a class="btn btn-primary btn-sm" id="loyalty-cart-link" href="/warenkorb/">{t.accountDash.loyaltyGoToCart}</a>
</div>
</div>
<p id="loyalty-status-text"></p>
<ul class="loyalty-rules">
<li><span aria-hidden="true">✅</span> {t.accountDash.loyaltyRuleValid}</li>
<li><span aria-hidden="true">❌</span> {t.accountDash.loyaltyRuleInvalid}</li>
<li><span aria-hidden="true">🔁</span> {t.accountDash.loyaltyRuleReset}</li>
</ul>
</div>
</div>
</div>
</section>
{/* Abosystem: drei Mitgliedschaftsstufen mit dauerhaftem Rabatt + Teddy-Prämien. Preise/Rabatte
kommen aus src/content/abo-einstellungen.json (admin-editierbar via Decap CMS), Status/
Fortschritt/Benachrichtigungen komplett aus dem Konto (siehe scripts/account.ts) und werden
per JS befüllt (Skript unten) — hier nur das Gerüst mit sinnvollen Leerzuständen. */}
<section class="section-tight" id="abo">
<div class="container">
<h2>{t.accountDash.aboTitle}</h2>
<p class="lead">{t.accountDash.aboIntro}</p>
<div class="abo-plans grid grid-3">
{ABO_STUFEN.map((stufe) => {
const cfg = aboStufenKonfig(stufe);
const labels = aboPlanLabels[stufe];
return (
<div class={`card abo-plan-card abo-plan-${stufe}`} data-abo-plan={stufe} data-abo-plan-aktiv={cfg.aktiv ? "1" : "0"}>
<span class="abo-plan-badge" data-abo-active-badge style="display:none;">{t.accountDash.aboAktivesLabel}</span>
<span class="abo-plan-icon" aria-hidden="true">{aboPlanIcon[stufe]}</span>
<h3>{labels.name}</h3>
<p class="abo-plan-price">{formatPrice(cfg.preis, lang)} <span class="small">{t.accountDash.aboPreisSuffix}</span></p>
<span class="abo-plan-discount-chip">💰 {cfg.rabattProzent}%</span>
<div class="abo-plan-rewards">
{cfg.kleinIntervallMonate > 0 && (
<span class="abo-plan-reward-chip">🧸 {t.accountDash.aboRewardEveryLabel.replace("{n}", String(cfg.kleinIntervallMonate))}</span>
)}
{cfg.grossIntervallMonate > 0 && (
<span class="abo-plan-reward-chip abo-plan-reward-chip-gross">🧸🧸 {t.accountDash.aboRewardEveryLabel.replace("{n}", String(cfg.grossIntervallMonate))}</span>
)}
</div>
<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>
<div class="abo-stat-grid">
<div class="abo-stat-tile">
<span class="abo-stat-icon" aria-hidden="true">🎫</span>
<span class="abo-stat-label">{t.accountDash.aboFieldStufe}</span>
<span class="abo-stat-value" id="abo-field-stufe"></span>
</div>
<div class="abo-stat-tile">
<span class="abo-stat-icon" aria-hidden="true">💶</span>
<span class="abo-stat-label">{t.accountDash.aboFieldPreis}</span>
<span class="abo-stat-value" id="abo-field-preis"></span>
</div>
<div class="abo-stat-tile abo-stat-tile-highlight">
<span class="abo-stat-icon" aria-hidden="true">💰</span>
<span class="abo-stat-label">{t.accountDash.aboFieldRabatt}</span>
<span class="abo-stat-value" id="abo-field-rabatt"></span>
</div>
<div class="abo-stat-tile">
<span class="abo-stat-icon" aria-hidden="true">📅</span>
<span class="abo-stat-label">{t.accountDash.aboFieldStart}</span>
<span class="abo-stat-value" id="abo-field-start"></span>
</div>
<div class="abo-stat-tile">
<span class="abo-stat-icon" aria-hidden="true">🔁</span>
<span class="abo-stat-label">{t.accountDash.aboFieldNaechsteZahlung}</span>
<span class="abo-stat-value" id="abo-field-naechste"></span>
</div>
<div class="abo-stat-tile">
<span class="abo-stat-icon" aria-hidden="true">⏱️</span>
<span class="abo-stat-label">{t.accountDash.aboFieldLaufzeit}</span>
<span class="abo-stat-value" id="abo-field-laufzeit"></span>
</div>
</div>
<div class="abo-reward-progress-block">
<div class="abo-reward-progress" id="abo-fortschritt-klein-wrap" style="display:none;">
<div class="abo-reward-progress-head">
<span>🧸 {t.accountDash.aboEinloesenKleinLabel}</span>
<span id="abo-fortschritt-klein-text"></span>
</div>
<div class="loyalty-progress-bar abo-reward-bar"><div class="loyalty-progress-fill" id="abo-fortschritt-klein-fill"></div></div>
</div>
<div class="abo-reward-progress" id="abo-fortschritt-gross-wrap" style="display:none;">
<div class="abo-reward-progress-head">
<span>🧸🧸 {t.accountDash.aboEinloesenGrossLabel}</span>
<span id="abo-fortschritt-gross-text"></span>
</div>
<div class="loyalty-progress-bar abo-reward-bar"><div class="loyalty-progress-fill abo-reward-bar-gross" id="abo-fortschritt-gross-fill"></div></div>
</div>
</div>
<div class="abo-erhalten-chip">
<span aria-hidden="true">🧸</span>
<span id="abo-praemien-erhalten"></span>
</div>
<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">
<div class="abo-reward-ready" id="abo-einloesen-klein-wrap" style="display:none;">
<span class="abo-reward-ready-icon" aria-hidden="true">🧸</span>
<div class="abo-reward-ready-body">
<strong>{t.accountDash.aboEinloesenKleinLabel}</strong>
<button type="button" class="btn btn-primary btn-sm" id="abo-einloesen-klein" data-einloesen="klein">{t.accountDash.aboEinloesenBtn}</button>
</div>
</div>
<div class="abo-reward-ready" id="abo-einloesen-gross-wrap" style="display:none;">
<span class="abo-reward-ready-icon" aria-hidden="true">🧸</span>
<div class="abo-reward-ready-body">
<strong>{t.accountDash.aboEinloesenGrossLabel}</strong>
<button type="button" class="btn btn-primary btn-sm" id="abo-einloesen-gross" data-einloesen="gross">{t.accountDash.aboEinloesenBtn}</button>
</div>
</div>
</div>
<p class="small" id="abo-eingeloest-hinweis" style="display:none; color: var(--c-accent);">{t.accountDash.aboEingeloestHinweis}</p>
</div>
<div class="abo-actions">
<button type="button" class="btn btn-outline btn-sm" id="abo-zahlung-sim-btn">{t.accountDash.aboZahlungSimBtn}</button>
<button type="button" class="btn btn-outline btn-sm abo-cancel-btn" id="abo-kuendigen-btn">{t.accountDash.aboKuendigenBtn}</button>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboZahlungSimHinweis}</p>
</div>
<div class="card" id="abo-benachrichtigungen-card">
<h3>{t.accountDash.aboBenachrichtigungenTitle}</h3>
<ul class="abo-notif-list" id="abo-notif-list">
<li class="small" id="abo-notif-empty">{t.accountDash.aboKeineBenachrichtigungen}</li>
</ul>
</div>
<p class="small loyalty-explainer">{t.accountDash.aboExplainer}</p>
</div>
</section>
</Layout>
<script define:vars={{ loginPath: "/en/konto/", fallbackName: "Customer", greetingTemplate: t.accountDash.greeting("{name}"), reviewedBadgeText: t.accountDash.reviewedBadge , invoiceLang: lang, invoiceLabels: { title: t.accountDash.invoiceTitle, numberLabel: t.accountDash.invoiceNumberLabel, dateLabel: t.accountDash.invoiceDateLabel, sellerLabel: t.accountDash.invoiceSellerLabel, billToLabel: t.accountDash.invoiceBillToLabel, itemLabel: t.accountDash.invoiceItemLabel, qtyLabel: t.accountDash.invoiceQtyLabel, unitPriceLabel: t.accountDash.invoiceUnitPriceLabel, sumLabel: t.accountDash.invoiceSumLabel, totalLabel: t.accountDash.orderTotal, vatNote: t.common.vatNote }, loyaltyProgressTemplate: t.accountDash.loyaltyProgressTemplate, loyaltyRemainingOne: t.accountDash.loyaltyRemainingOne, loyaltyRemainingOther: t.accountDash.loyaltyRemainingOther, loyaltyUnlockedTitle: t.accountDash.loyaltyUnlockedTitle, loyaltyUnlockedText: t.accountDash.loyaltyUnlockedText, loyaltyStatRedeemedNever: t.accountDash.loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix: t.accountDash.loyaltyStatRedeemedSuffix, loyaltyLastRedeemed: t.accountDash.loyaltyLastRedeemed, 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, nichtBuchbar: t.accountDash.aboNichtBuchbar, 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 } }, deleteAccountConfirm: t.accountDash.deleteAccountConfirm, deleteAccountError: t.accountDash.deleteAccountError }}>
window.__accountDashVars = { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError };
</script>
<script>
// Schnellnavigation als echte Filter-Tabs: Klick zeigt NUR den passenden Bereich, blendet den
// Rest aus (statt nur hinzuspringen). "Bestellungen" zeigt nur schon ANGEKOMMENE (abgeschlossene)
// Bestellungen, "Offene Sendungen" nur die noch nicht angekommenen — echte Trennung statt
// Überschneidung.
const OFFENE_STATUS = ["bezahlt", "bearbeitung", "versandVorbereitet", "versendet"];
const navItems = Array.from(document.querySelectorAll(".account-nav-item")) as HTMLButtonElement[];
const bestellungenSection = document.getElementById("bestellungen");
const wunschlisteSection = document.getElementById("wunschliste");
const recentSection = document.getElementById("zuletzt-angesehen");
const datenSection = document.getElementById("meine-daten");
const treuebonusSection = document.getElementById("treuebonus");
const aboSection = document.getElementById("abo");
const orderCards = Array.from(document.querySelectorAll(".order-card"));
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
// Beim Laden ist noch KEIN Knopf gedrückt — solange nichts angeklickt wurde, steht direkt
// "Zuletzt angesehen" da, alle anderen (per Tab umschaltbaren) Bereiche bleiben versteckt.
function setActiveTab(tab: string | null) {
navItems.forEach((item) => item.classList.toggle("is-active", item.dataset.tab === tab));
if (bestellungenSection) bestellungenSection.style.display = "none";
if (wunschlisteSection) wunschlisteSection.style.display = "none";
if (datenSection) datenSection.style.display = "none";
if (treuebonusSection) treuebonusSection.style.display = "none";
if (aboSection) aboSection.style.display = "none";
if (recentSection) recentSection.style.display = "none";
if (tab === null) {
if (recentSection) recentSection.style.display = "";
return;
}
if (tab === "wunschliste") {
if (wunschlisteSection) wunschlisteSection.style.display = "";
return;
}
if (tab === "daten") {
if (datenSection) datenSection.style.display = "";
return;
}
if (tab === "treuebonus") {
if (treuebonusSection) treuebonusSection.style.display = "";
return;
}
if (tab === "abo") {
if (aboSection) aboSection.style.display = "";
return;
}
if (bestellungenSection) bestellungenSection.style.display = "";
let visibleCount = 0;
orderCards.forEach((card) => {
const status = OFFENE_STATUS.find((s) => card.classList.contains("order-card-" + s));
const show = tab === "offene" ? !!status : card.classList.contains("order-card-abgeschlossen");
(card as HTMLElement).style.display = show ? "" : "none";
if (show) visibleCount++;
});
if (emptyNote) {
if (visibleCount === 0 && (tab === "offene" || tab === "bestellungen")) {
emptyNote.textContent = tab === "offene" ? emptyNote.dataset.emptyOpen ?? "" : emptyNote.dataset.emptyArrived ?? "";
emptyNote.style.display = "block";
} else {
emptyNote.style.display = "none";
}
}
}
navItems.forEach((item) => {
item.addEventListener("click", () => { if (item.dataset.tab) setActiveTab(item.dataset.tab); });
});
// Der "Meine Daten"-Button in der Kopfkarte springt direkt in den Profil-Bereich. Der Zugriff
// aufs Abo läuft jetzt nur noch über die "Mein Abo"-Nav-Kachel weiter unten.
document.getElementById("dash-daten-btn")?.addEventListener("click", () => {
setActiveTab("daten");
datenSection?.scrollIntoView({ behavior: "smooth", block: "start" });
});
setActiveTab(null);
</script>
<script>
import { getAccount, isLoggedIn, login, setName, logout, setUsername, setPhoto, hatBestellungBewertet, anzeigeName, treueFortschritt, aboStatus, aboAbschliessen, aboZahlungSimulieren, aboKuendigen, teddyEinloesen } from "../../../scripts/account";
import { rechnungAlsPdfHerunterladen } from "../../../scripts/invoice-pdf";
import { fuegeTeddyGratisHinzu } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { aboStufenKonfig } from "../../../data/abo";
const { loginPath, fallbackName, greetingTemplate, reviewedBadgeText, invoiceLang, invoiceLabels, loyaltyProgressTemplate, loyaltyRemainingOne, loyaltyRemainingOther, loyaltyUnlockedTitle, loyaltyUnlockedText, loyaltyStatRedeemedNever, loyaltyStatRedeemedSuffix, loyaltyLastRedeemed, aboLang, aboLabels, deleteAccountConfirm, deleteAccountError } = (window as any).__accountDashVars;
// Echte Server-Sitzung (Google-/PayPal-Login, siehe functions/_shared/customer-auth.js)
// einbinden: falls vorhanden, in das bestehende localStorage-Demo-Kontosystem übernehmen,
// damit das unveränderte Dashboard unten korrekt mit echten Daten befüllt wird. Läuft VOR dem
// Zugriffsschutz-Check, damit ein frisch per OAuth angemeldeter Besuch nicht fälschlich zurück
// zur Login-Seite geschickt wird.
let istEchteSitzung = false;
try {
const res = await fetch("/api/account/me");
const data = await res.json();
if (data?.ok && data.customer) {
istEchteSitzung = true;
if (!isLoggedIn()) login(data.customer.email);
if (data.customer.name) setName(data.customer.name);
}
} catch {
// Kein Backend erreichbar / keine Sitzung — Demo-/Vorschau-Konto (falls vorhanden) läuft normal weiter.
}
// Zugriffsschutz: diese Seite ist nur für angemeldete Kund:innen gedacht (echt oder Demo-
// Vorschau). Bei fehlender Anmeldung sofort zur Login-Seite weitergeleitet.
if (!isLoggedIn()) {
const note = document.getElementById("not-logged-in-note");
const preview = document.getElementById("preview-note");
if (note) note.style.display = "block";
if (preview) preview.style.display = "none";
setTimeout(() => { window.location.href = loginPath; }, 900);
} else {
function avatarHtml(username: string, photo: string): { bg: string; text: string } {
if (photo) return { bg: `url(${photo})`, text: "" };
return { bg: "", text: (username || "?").trim().charAt(0).toUpperCase() };
}
function renderProfil() {
const a = getAccount();
const greetingEl = document.getElementById("dash-greeting");
if (greetingEl) greetingEl.textContent = greetingTemplate.replace("{name}", anzeigeName(a) || fallbackName);
const emailEl = document.getElementById("profile-email");
if (emailEl) emailEl.textContent = a.email || "";
const nameDisplay = document.getElementById("profile-name-display");
if (nameDisplay) nameDisplay.textContent = a.name || "";
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
if (usernameInput && document.activeElement !== usernameInput) usernameInput.value = a.username;
const { bg, text } = avatarHtml(anzeigeName(a), a.photo);
[document.getElementById("dash-avatar"), document.getElementById("profile-avatar")].forEach((el) => {
if (!el) return;
el.style.backgroundImage = bg;
el.textContent = text;
});
// Empfängername in allen (schon gerenderten) Rechnungs-Dialogen live mit aktualisieren.
document.querySelectorAll<HTMLElement>(".invoice-billto-name").forEach((el) => {
el.textContent = anzeigeName(a) || fallbackName;
});
}
renderProfil();
window.addEventListener("account:changed", renderProfil);
// Treuebonus: Fortschritt/Status live aus dem Konto rendern (Nav-Kachel + Inhaltsbereich).
function renderLoyalty() {
const setText = (id: string, text: string) => { const el = document.getElementById(id); if (el) el.textContent = text; };
const f = treueFortschritt(getAccount());
const navValue = document.getElementById("loyalty-nav-value");
if (navValue) navValue.textContent = f.gesperrt ? "🔒" : `${f.aktuelle}/${f.ziel}`;
document.querySelector(".account-nav-loyalty")?.classList.toggle("is-ready", f.aktiv && !f.gesperrt);
const lockedContent = document.getElementById("loyalty-locked-content");
const unlockedContent = document.getElementById("loyalty-unlocked-content");
if (lockedContent) lockedContent.style.display = f.gesperrt ? "flex" : "none";
if (unlockedContent) unlockedContent.style.display = f.gesperrt ? "none" : "flex";
if (f.gesperrt) return;
const fill = document.getElementById("loyalty-bar-fill") as HTMLElement | null;
if (fill) fill.style.width = `${Math.min(100, (f.aktuelle / f.ziel) * 100)}%`;
const progressText = document.getElementById("loyalty-progress-text");
if (progressText) {
progressText.textContent = loyaltyProgressTemplate
.replace("{cur}", String(f.aktuelle))
.replace("{ziel}", String(f.ziel));
}
setText("loyalty-stat-collected", `${f.aktuelle}/${f.ziel}`);
setText("loyalty-stat-missing", String(f.fehlende));
setText("loyalty-stat-redeemed", f.eingeloestGesamt > 0 ? `${f.eingeloestGesamt}${loyaltyStatRedeemedSuffix}` : loyaltyStatRedeemedNever);
const lastRedeemedEl = document.getElementById("loyalty-last-redeemed");
if (lastRedeemedEl) {
if (f.letzteEinloesungDatum) {
lastRedeemedEl.textContent = loyaltyLastRedeemed.replace("{date}", f.letzteEinloesungDatum);
lastRedeemedEl.style.display = "";
} else {
lastRedeemedEl.style.display = "none";
}
}
const readyBlock = document.getElementById("loyalty-ready-block");
const statusText = document.getElementById("loyalty-status-text");
if (f.aktiv) {
if (readyBlock) readyBlock.style.display = "flex";
if (statusText) statusText.textContent = "";
} else {
if (readyBlock) readyBlock.style.display = "none";
if (statusText) {
statusText.textContent = f.fehlende === 1 ? loyaltyRemainingOne : loyaltyRemainingOther.replace("{n}", String(f.fehlende));
}
}
}
renderLoyalty();
window.addEventListener("account:changed", renderLoyalty);
// Im gesperrten Treuebonus-Zustand direkt zum Abo-Bereich springen.
document.getElementById("loyalty-locked-abo-btn")?.addEventListener("click", () => {
setActiveTab("abo");
document.getElementById("abo")?.scrollIntoView({ behavior: "smooth", block: "start" });
});
// 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 };
// Icon + Farbkategorie je Benachrichtigungs-Typ — rein optisch, keine Übersetzung nötig
// (die Schlüssel sind sprachneutrale Konstanten aus scripts/account.ts).
const NOTIF_ICON: Record<string, string> = {
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: "🎁",
};
const NOTIF_KATEGORIE: Record<string, string> = {
abo_abgeschlossen: "erfolg",
abo_gewechselt: "info",
abo_zahlung_erfolgreich: "erfolg",
abo_teddy_klein_frei: "praemie",
abo_teddy_gross_frei: "praemie",
abo_bald_praemie: "warnung",
abo_gekuendigt: "abbruch",
abo_teddy_klein_eingeloest: "praemie",
abo_teddy_gross_eingeloest: "praemie",
};
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 buchbar = card.dataset.aboPlanAktiv !== "0";
card.classList.toggle("is-not-bookable", !buchbar && !isCurrent);
const btn = card.querySelector("[data-abo-choose]") as HTMLButtonElement | null;
if (btn) {
btn.disabled = isCurrent || (!buchbar && !isCurrent);
btn.textContent = isCurrent
? aboLabels.aktivBadge
: !buchbar
? aboLabels.nichtBuchbar
: (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");
}
// Echte Fortschrittsbalken statt reinem Text — Prozent ergibt sich aus dem admin-
// konfigurierten Intervall (aboStufenKonfig) minus der noch fehlenden Monate.
const stufenKonfig = aboStufenKonfig(status.stufe);
const fortschrittKleinWrap = document.getElementById("abo-fortschritt-klein-wrap");
if (fortschrittKleinWrap) {
if (status.naechsterKleinIn != null && stufenKonfig.kleinIntervallMonate > 0) {
fortschrittKleinWrap.style.display = "block";
setText("abo-fortschritt-klein-text", aboLabels.fortschrittKleinTemplate.replace("{n}", String(status.naechsterKleinIn)));
const pct = Math.min(100, Math.max(0, ((stufenKonfig.kleinIntervallMonate - status.naechsterKleinIn) / stufenKonfig.kleinIntervallMonate) * 100));
const fill = document.getElementById("abo-fortschritt-klein-fill") as HTMLElement | null;
if (fill) fill.style.width = `${pct}%`;
} else fortschrittKleinWrap.style.display = "none";
}
const fortschrittGrossWrap = document.getElementById("abo-fortschritt-gross-wrap");
if (fortschrittGrossWrap) {
if (status.naechsterGrossIn != null && stufenKonfig.grossIntervallMonate > 0) {
fortschrittGrossWrap.style.display = "block";
setText("abo-fortschritt-gross-text", aboLabels.fortschrittGrossTemplate.replace("{n}", String(status.naechsterGrossIn)));
const pct = Math.min(100, Math.max(0, ((stufenKonfig.grossIntervallMonate - status.naechsterGrossIn) / stufenKonfig.grossIntervallMonate) * 100));
const fill = document.getElementById("abo-fortschritt-gross-fill") as HTMLElement | null;
if (fill) fill.style.width = `${pct}%`;
} else fortschrittGrossWrap.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 einloesenKleinWrap = document.getElementById("abo-einloesen-klein-wrap") as HTMLElement | null;
const einloesenGrossWrap = document.getElementById("abo-einloesen-gross-wrap") as HTMLElement | null;
const keinePraemien = document.getElementById("abo-keine-praemien") as HTMLElement | null;
if (einloesenKleinWrap) einloesenKleinWrap.style.display = hasKlein ? "flex" : "none";
if (einloesenGrossWrap) einloesenGrossWrap.style.display = hasGross ? "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");
const kategorie = NOTIF_KATEGORIE[n.key] ?? "info";
li.className = `abo-notif-item abo-notif-${kategorie}` + (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.innerHTML = `
<span class="abo-notif-icon" aria-hidden="true">${NOTIF_ICON[n.key] ?? "🔔"}</span>
<span class="abo-notif-body"><span class="abo-notif-text small">${text}</span><span class="abo-notif-date small">${n.datum}</span></span>
`;
list.appendChild(li);
});
}
}
}
renderAbo();
window.addEventListener("account:changed", renderAbo);
document.querySelectorAll<HTMLButtonElement>("[data-abo-choose]").forEach((btn) => {
btn.addEventListener("click", () => {
const stufe = btn.dataset.aboChoose as "klein" | "mittel" | "gross" | undefined;
if (!stufe) return;
aboAbschliessen(stufe);
});
});
document.getElementById("abo-zahlung-sim-btn")?.addEventListener("click", () => { aboZahlungSimulieren(); });
document.getElementById("abo-kuendigen-btn")?.addEventListener("click", () => {
if (window.confirm(aboLabels.kuendigenConfirm)) aboKuendigen();
});
document.querySelectorAll<HTMLButtonElement>("[data-einloesen]").forEach((btn) => {
btn.addEventListener("click", () => {
const art = btn.dataset.einloesen as "klein" | "gross" | undefined;
if (!art) return;
teddyEinloesen(art);
fuegeTeddyGratisHinzu(art);
const hinweis = document.getElementById("abo-eingeloest-hinweis");
if (hinweis) {
hinweis.style.display = "block";
setTimeout(() => { hinweis.style.display = "none"; }, 2500);
}
});
});
// Benutzername ändern
const saveBtn = document.getElementById("profile-username-save");
const usernameInput = document.getElementById("profile-username") as HTMLInputElement | null;
const savedStatus = document.getElementById("profile-username-status");
saveBtn?.addEventListener("click", () => {
if (!usernameInput?.value.trim()) return;
setUsername(usernameInput.value);
if (savedStatus) {
savedStatus.style.display = "inline";
setTimeout(() => { savedStatus.style.display = "none"; }, 2000);
}
});
// Profilfoto hochladen (wird als data:-URL im localStorage abgelegt — reicht für diese
// Demo völlig aus, in Phase 2 würde ein echter Datei-Upload an den Server dahinterstehen)
const photoInput = document.getElementById("profile-photo-input") as HTMLInputElement | null;
photoInput?.addEventListener("change", () => {
const file = photoInput.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => setPhoto(String(reader.result));
reader.readAsDataURL(file);
});
document.getElementById("profile-photo-remove")?.addEventListener("click", () => setPhoto(""));
// Logout direkt aus dem Dashboard heraus — löscht sowohl das lokale Demo-Konto als auch,
// falls vorhanden, die echte serverseitige Sitzung (Cookie).
document.getElementById("dash-logout-btn")?.addEventListener("click", () => {
logout();
if (istEchteSitzung) {
fetch("/api/account/logout", { method: "POST" }).finally(() => { window.location.href = loginPath; });
} else {
window.location.href = loginPath;
}
});
// Echte DSGVO-Selbstbedienungs-Rechte (Art. 15/17/20 DSGVO) — nur sichtbar/aktiv bei einer
// echten Server-Sitzung, siehe istEchteSitzung oben.
const dsgvoBlock = document.getElementById("profile-dsgvo-actions");
if (istEchteSitzung && dsgvoBlock) dsgvoBlock.style.display = "flex";
document.getElementById("dsgvo-delete-btn")?.addEventListener("click", async () => {
if (!window.confirm(deleteAccountConfirm)) return;
const errorEl = document.getElementById("dsgvo-delete-error");
try {
const res = await fetch("/api/account/delete", { method: "POST" });
const data = await res.json();
if (!data?.ok) throw new Error("delete failed");
logout();
window.location.href = loginPath;
} catch {
if (errorEl) errorEl.style.display = "block";
}
});
// Rechnung direkt auf der Seite ansehen: natives <dialog> pro Bestellung, geöffnet/geschlossen
// per Button — kein Download nötig, kein Verlassen der Seite.
document.querySelectorAll<HTMLButtonElement>("[data-open-invoice]").forEach((btn) => {
btn.addEventListener("click", () => {
const id = btn.dataset.openInvoice;
const dialog = id ? (document.getElementById(id) as HTMLDialogElement | null) : null;
dialog?.showModal();
});
});
// Rechnung als echte PDF-Datei herunterladen (siehe scripts/invoice-pdf.ts) — dieselben
// Daten wie im "Rechnung ansehen"-Dialog, per dynamic import erst beim Klick nachgeladen.
document.querySelectorAll<HTMLButtonElement>("[data-download-invoice]").forEach((btn) => {
btn.addEventListener("click", async () => {
const artikel = JSON.parse(btn.dataset.invoiceArtikel || "[]");
btn.disabled = true;
btn.textContent = btn.dataset.labelLoading || "…";
try {
await rechnungAlsPdfHerunterladen({
nummer: btn.dataset.downloadInvoice || "",
datum: btn.dataset.invoiceDatum || "",
summe: Number(btn.dataset.invoiceSumme || 0),
artikel,
empfaenger: anzeigeName(getAccount()) || fallbackName,
lang: invoiceLang,
labels: invoiceLabels,
});
} finally {
btn.disabled = false;
btn.textContent = btn.dataset.labelDefault || "";
}
});
});
document.querySelectorAll<HTMLDialogElement>(".invoice-dialog").forEach((dialog) => {
dialog.querySelectorAll<HTMLButtonElement>("[data-close-invoice]").forEach((btn) => {
btn.addEventListener("click", () => dialog.close());
});
// Klick auf den abgedunkelten Hintergrund schließt den Dialog ebenfalls.
dialog.addEventListener("click", (e) => {
if (e.target === dialog) dialog.close();
});
});
// Pro Bestellposition: Button "Rezension schreiben" durch "✓ Bewertet" ersetzen, sobald für
// genau diese Bestellung+Produkt schon eine Rezension existiert (siehe scripts/reviews.ts,
// wird beim Absenden des Formulars auf der Produktseite markiert).
document.querySelectorAll<HTMLElement>("[data-order-review]").forEach((el) => {
const bestellnummer = el.dataset.bestellnummer!;
const slug = el.dataset.slug!;
if (hatBestellungBewertet(bestellnummer, slug)) {
el.innerHTML = "";
const badge = document.createElement("span");
badge.className = "badge review-done-badge";
badge.textContent = reviewedBadgeText;
el.appendChild(badge);
}
});
}
</script>