+
+ {/* Bestellungen, die aelter als ein Monat sind, stehen nicht dauerhaft in der Liste, sondern
+ gebuendelt hinter diesem Knopf — sonst wird die Uebersicht mit den Jahren unuebersichtlich
+ lang. Aufgeklappt kann man sie gezielt nach Bestellnummer oder Artikel durchsuchen.
+ WICHTIG: Ob eine Bestellung "aelter als ein Monat" ist, wird ERST IM BROWSER berechnet
+ (siehe Skript unten). Die Seite ist statisch vorgebaut — eine schon beim Bauen berechnete
+ Grenze wuerde mit jedem Tag ungenauer werden. */}
+
+
+
+
+
+ 🔍
+
+
+
{t.accountDash.olderOrdersNoHits}
+
+
@@ -554,9 +623,97 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
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 orderCards = Array.from(document.querySelectorAll(".order-card")) as HTMLElement[];
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
+ /* ---- Aeltere Bestellungen (> 1 Monat) buendeln + durchsuchbar machen -------------------
+ Die Grenze wird hier im Browser gezogen, nicht beim Bauen der Seite: "vor einem Monat"
+ wandert taeglich mit, eine fest eingebackene Grenze waere schon am naechsten Tag falsch.
+ Gerechnet wird mit echten Kalendermonaten (setMonth(-1)), nicht mit "30 Tagen". */
+ const archivBlock = document.getElementById("order-archive");
+ const archivToggle = document.getElementById("order-archive-toggle") as HTMLButtonElement | null;
+ const archivLabel = document.getElementById("order-archive-label");
+ const archivPanel = document.getElementById("order-archive-panel");
+ const archivSuche = document.getElementById("order-archive-search") as HTMLInputElement | null;
+ const archivNoHits = document.getElementById("order-archive-nohits");
+
+ const monatsGrenze = new Date();
+ monatsGrenze.setMonth(monatsGrenze.getMonth() - 1);
+ monatsGrenze.setHours(0, 0, 0, 0);
+
+ function istAelterAlsEinMonat(card: HTMLElement) {
+ const iso = card.dataset.orderIso;
+ if (!iso) return false;
+ const d = new Date(iso + "T00:00:00");
+ return !isNaN(d.getTime()) && d < monatsGrenze;
+ }
+
+ const alteKarten = orderCards.filter(istAelterAlsEinMonat);
+ const aktuelleKarten = orderCards.filter((c) => !istAelterAlsEinMonat(c));
+
+ // Alte Karten in den Aufklapp-Bereich verschieben. Sie bleiben dabei im Dokument, also
+ // funktionieren Rechnung/Rezension/Sendungsverfolgung darin unveraendert weiter.
+ if (archivPanel) alteKarten.forEach((c) => archivPanel.appendChild(c));
+
+ let aktuellerTab: string | null = null;
+ let archivOffen = false;
+ let archivTrefferGesamt = 0;
+
+ function passtZuTab(card: HTMLElement, tab: string | null) {
+ const status = OFFENE_STATUS.find((s) => card.classList.contains("order-card-" + s));
+ return tab === "offene" ? !!status : card.classList.contains("order-card-abgeschlossen");
+ }
+
+ // Durchsucht Bestellnummer UND Artikelnamen — genau das, wonach man eine alte Bestellung
+ // typischerweise sucht ("wo war nochmal die Decke?").
+ function passtZurSuche(card: HTMLElement, begriff: string) {
+ if (!begriff) return true;
+ const nummer = card.querySelector(".order-no")?.textContent ?? "";
+ const artikel = Array.from(card.querySelectorAll(".order-items li")).map((li) => li.textContent ?? "").join(" ");
+ const datum = card.querySelector(".order-meta")?.textContent ?? "";
+ return (nummer + " " + artikel + " " + datum).toLowerCase().includes(begriff);
+ }
+
+ function aktualisiereArchiv() {
+ if (!archivBlock) return;
+ const begriff = (archivSuche?.value ?? "").trim().toLowerCase();
+
+ // Wie viele aeltere Bestellungen gehoeren ueberhaupt zum aktuellen Tab?
+ const imTab = alteKarten.filter((c) => passtZuTab(c, aktuellerTab));
+ const treffer = imTab.filter((c) => passtZurSuche(c, begriff));
+ archivTrefferGesamt = treffer.length;
+
+ // Sichtbarkeit der einzelnen alten Karten
+ alteKarten.forEach((c) => {
+ const zeigen = archivOffen && imTab.includes(c) && passtZurSuche(c, begriff);
+ c.style.display = zeigen ? "" : "none";
+ });
+
+ // Gibt es im aktuellen Tab gar keine alten Bestellungen, verschwindet der ganze Knopf.
+ archivBlock.hidden = imTab.length === 0;
+ if (archivLabel && archivToggle) {
+ archivLabel.textContent = archivOffen
+ ? (archivToggle.dataset.labelHide ?? "")
+ : `${archivToggle.dataset.labelShow ?? ""} (${imTab.length})`;
+ }
+ if (archivNoHits) archivNoHits.hidden = !(archivOffen && begriff !== "" && treffer.length === 0);
+ }
+
+ archivToggle?.addEventListener("click", () => {
+ archivOffen = !archivOffen;
+ archivToggle.setAttribute("aria-expanded", String(archivOffen));
+ archivBlock?.classList.toggle("is-open", archivOffen);
+ if (archivPanel) archivPanel.hidden = !archivOffen;
+ aktualisiereArchiv();
+ if (archivOffen) archivSuche?.focus();
+ });
+
+ archivSuche?.addEventListener("input", aktualisiereArchiv);
+ // Escape leert die Suche, statt (wie sonst bei type="search") nur das Feld zu leeren.
+ archivSuche?.addEventListener("keydown", (e) => {
+ if ((e as KeyboardEvent).key === "Escape") { archivSuche.value = ""; aktualisiereArchiv(); }
+ });
+
// 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) {
@@ -595,15 +752,19 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
}
if (bestellungenSection) bestellungenSection.style.display = "";
+ aktuellerTab = tab;
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");
+ aktuelleKarten.forEach((card) => {
+ const show = passtZuTab(card, tab);
(card as HTMLElement).style.display = show ? "" : "none";
if (show) visibleCount++;
});
+ aktualisiereArchiv();
if (emptyNote) {
- if (visibleCount === 0 && (tab === "offene" || tab === "bestellungen")) {
+ // Aeltere (archivierte) Bestellungen zaehlen hier bewusst mit: Wer im Archiv Treffer hat,
+ // soll nicht faelschlich "keine Bestellungen" lesen.
+ const gesamt = visibleCount + archivTrefferGesamt;
+ if (gesamt === 0 && (tab === "offene" || tab === "bestellungen")) {
emptyNote.textContent = tab === "offene" ? emptyNote.dataset.emptyOpen ?? "" : emptyNote.dataset.emptyArrived ?? "";
emptyNote.style.display = "block";
} else {
diff --git a/src/pages/en/konto/angemeldet.astro b/src/pages/en/konto/angemeldet.astro
index 05a9b27..8a8224e 100644
--- a/src/pages/en/konto/angemeldet.astro
+++ b/src/pages/en/konto/angemeldet.astro
@@ -16,10 +16,13 @@ const t = useTranslations(lang);
// 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.
+// Das Bestelldatum steht bewusst als ISO-Datum (JJJJ-MM-TT) da: Daraus wird sowohl die Anzeige
+// abgeleitet als auch im Skript unten berechnet, welche Bestellung "aelter als ein Monat" ist.
+// So gibt es nur EINE Quelle fuer das Datum, die nicht auseinanderlaufen kann.
const beispielBestellungenRoh = [
{
nummer: "VDB-2026-0184",
- datum: "28.07.2026",
+ iso: "2026-07-28",
status: "versendet" as const,
tracking: "00340434161094015791",
artikel: [
@@ -29,18 +32,48 @@ const beispielBestellungenRoh = [
},
{
nummer: "VDB-2026-0157",
- datum: "12.07.2026",
+ iso: "2026-07-12",
status: "abgeschlossen" as const,
tracking: null,
artikel: [{ slug: "haekelnadel-set-ergonomisch", menge: 1 }],
},
+ // Aeltere Beispielbestellungen: landen automatisch im aufklappbaren Archiv-Bereich weiter
+ // unten, damit die Liste oben kurz bleibt und man Aeltere gezielt suchen kann.
+ {
+ nummer: "VDB-2026-0102",
+ iso: "2026-05-21",
+ status: "abgeschlossen" as const,
+ tracking: null,
+ artikel: [{ slug: "haekeldecke-boho", menge: 1 }],
+ },
+ {
+ nummer: "VDB-2026-0061",
+ iso: "2026-03-14",
+ status: "abgeschlossen" as const,
+ tracking: null,
+ artikel: [{ slug: "makramee-ohrringe", menge: 2 }],
+ },
+ {
+ nummer: "VDB-2025-0488",
+ iso: "2025-12-02",
+ status: "abgeschlossen" as const,
+ tracking: null,
+ artikel: [
+ { slug: "bastel-starterset", menge: 1 },
+ { slug: "baumwollgarn-bundle", menge: 1 },
+ ],
+ },
];
+const anzeigeDatum = (iso: string) => {
+ const [jahr, monat, tag] = iso.split("-");
+ return `${tag}.${monat}.${jahr}`;
+};
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) };
+ return { ...b, datum: anzeigeDatum(b.iso), artikel, summe: artikel.reduce((s, a) => s + a.preis * a.menge, 0) };
});
const statusLabel = {
@@ -147,7 +180,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
+
+ {/* Bestellungen, die aelter als ein Monat sind, stehen nicht dauerhaft in der Liste, sondern
+ gebuendelt hinter diesem Knopf — sonst wird die Uebersicht mit den Jahren unuebersichtlich
+ lang. Aufgeklappt kann man sie gezielt nach Bestellnummer oder Artikel durchsuchen.
+ WICHTIG: Ob eine Bestellung "aelter als ein Monat" ist, wird ERST IM BROWSER berechnet
+ (siehe Skript unten). Die Seite ist statisch vorgebaut — eine schon beim Bauen berechnete
+ Grenze wuerde mit jedem Tag ungenauer werden. */}
+
+
+
+
+
+ 🔍
+
+
+
{t.accountDash.olderOrdersNoHits}
+
+
@@ -554,9 +623,97 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
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 orderCards = Array.from(document.querySelectorAll(".order-card")) as HTMLElement[];
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
+ /* ---- Aeltere Bestellungen (> 1 Monat) buendeln + durchsuchbar machen -------------------
+ Die Grenze wird hier im Browser gezogen, nicht beim Bauen der Seite: "vor einem Monat"
+ wandert taeglich mit, eine fest eingebackene Grenze waere schon am naechsten Tag falsch.
+ Gerechnet wird mit echten Kalendermonaten (setMonth(-1)), nicht mit "30 Tagen". */
+ const archivBlock = document.getElementById("order-archive");
+ const archivToggle = document.getElementById("order-archive-toggle") as HTMLButtonElement | null;
+ const archivLabel = document.getElementById("order-archive-label");
+ const archivPanel = document.getElementById("order-archive-panel");
+ const archivSuche = document.getElementById("order-archive-search") as HTMLInputElement | null;
+ const archivNoHits = document.getElementById("order-archive-nohits");
+
+ const monatsGrenze = new Date();
+ monatsGrenze.setMonth(monatsGrenze.getMonth() - 1);
+ monatsGrenze.setHours(0, 0, 0, 0);
+
+ function istAelterAlsEinMonat(card: HTMLElement) {
+ const iso = card.dataset.orderIso;
+ if (!iso) return false;
+ const d = new Date(iso + "T00:00:00");
+ return !isNaN(d.getTime()) && d < monatsGrenze;
+ }
+
+ const alteKarten = orderCards.filter(istAelterAlsEinMonat);
+ const aktuelleKarten = orderCards.filter((c) => !istAelterAlsEinMonat(c));
+
+ // Alte Karten in den Aufklapp-Bereich verschieben. Sie bleiben dabei im Dokument, also
+ // funktionieren Rechnung/Rezension/Sendungsverfolgung darin unveraendert weiter.
+ if (archivPanel) alteKarten.forEach((c) => archivPanel.appendChild(c));
+
+ let aktuellerTab: string | null = null;
+ let archivOffen = false;
+ let archivTrefferGesamt = 0;
+
+ function passtZuTab(card: HTMLElement, tab: string | null) {
+ const status = OFFENE_STATUS.find((s) => card.classList.contains("order-card-" + s));
+ return tab === "offene" ? !!status : card.classList.contains("order-card-abgeschlossen");
+ }
+
+ // Durchsucht Bestellnummer UND Artikelnamen — genau das, wonach man eine alte Bestellung
+ // typischerweise sucht ("wo war nochmal die Decke?").
+ function passtZurSuche(card: HTMLElement, begriff: string) {
+ if (!begriff) return true;
+ const nummer = card.querySelector(".order-no")?.textContent ?? "";
+ const artikel = Array.from(card.querySelectorAll(".order-items li")).map((li) => li.textContent ?? "").join(" ");
+ const datum = card.querySelector(".order-meta")?.textContent ?? "";
+ return (nummer + " " + artikel + " " + datum).toLowerCase().includes(begriff);
+ }
+
+ function aktualisiereArchiv() {
+ if (!archivBlock) return;
+ const begriff = (archivSuche?.value ?? "").trim().toLowerCase();
+
+ // Wie viele aeltere Bestellungen gehoeren ueberhaupt zum aktuellen Tab?
+ const imTab = alteKarten.filter((c) => passtZuTab(c, aktuellerTab));
+ const treffer = imTab.filter((c) => passtZurSuche(c, begriff));
+ archivTrefferGesamt = treffer.length;
+
+ // Sichtbarkeit der einzelnen alten Karten
+ alteKarten.forEach((c) => {
+ const zeigen = archivOffen && imTab.includes(c) && passtZurSuche(c, begriff);
+ c.style.display = zeigen ? "" : "none";
+ });
+
+ // Gibt es im aktuellen Tab gar keine alten Bestellungen, verschwindet der ganze Knopf.
+ archivBlock.hidden = imTab.length === 0;
+ if (archivLabel && archivToggle) {
+ archivLabel.textContent = archivOffen
+ ? (archivToggle.dataset.labelHide ?? "")
+ : `${archivToggle.dataset.labelShow ?? ""} (${imTab.length})`;
+ }
+ if (archivNoHits) archivNoHits.hidden = !(archivOffen && begriff !== "" && treffer.length === 0);
+ }
+
+ archivToggle?.addEventListener("click", () => {
+ archivOffen = !archivOffen;
+ archivToggle.setAttribute("aria-expanded", String(archivOffen));
+ archivBlock?.classList.toggle("is-open", archivOffen);
+ if (archivPanel) archivPanel.hidden = !archivOffen;
+ aktualisiereArchiv();
+ if (archivOffen) archivSuche?.focus();
+ });
+
+ archivSuche?.addEventListener("input", aktualisiereArchiv);
+ // Escape leert die Suche, statt (wie sonst bei type="search") nur das Feld zu leeren.
+ archivSuche?.addEventListener("keydown", (e) => {
+ if ((e as KeyboardEvent).key === "Escape") { archivSuche.value = ""; aktualisiereArchiv(); }
+ });
+
// 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) {
@@ -595,15 +752,19 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
}
if (bestellungenSection) bestellungenSection.style.display = "";
+ aktuellerTab = tab;
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");
+ aktuelleKarten.forEach((card) => {
+ const show = passtZuTab(card, tab);
(card as HTMLElement).style.display = show ? "" : "none";
if (show) visibleCount++;
});
+ aktualisiereArchiv();
if (emptyNote) {
- if (visibleCount === 0 && (tab === "offene" || tab === "bestellungen")) {
+ // Aeltere (archivierte) Bestellungen zaehlen hier bewusst mit: Wer im Archiv Treffer hat,
+ // soll nicht faelschlich "keine Bestellungen" lesen.
+ const gesamt = visibleCount + archivTrefferGesamt;
+ if (gesamt === 0 && (tab === "offene" || tab === "bestellungen")) {
emptyNote.textContent = tab === "offene" ? emptyNote.dataset.emptyOpen ?? "" : emptyNote.dataset.emptyArrived ?? "";
emptyNote.style.display = "block";
} else {
diff --git a/src/pages/fr/konto/angemeldet.astro b/src/pages/fr/konto/angemeldet.astro
index 5d61478..00ddcaf 100644
--- a/src/pages/fr/konto/angemeldet.astro
+++ b/src/pages/fr/konto/angemeldet.astro
@@ -16,10 +16,13 @@ const t = useTranslations(lang);
// 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.
+// Das Bestelldatum steht bewusst als ISO-Datum (JJJJ-MM-TT) da: Daraus wird sowohl die Anzeige
+// abgeleitet als auch im Skript unten berechnet, welche Bestellung "aelter als ein Monat" ist.
+// So gibt es nur EINE Quelle fuer das Datum, die nicht auseinanderlaufen kann.
const beispielBestellungenRoh = [
{
nummer: "VDB-2026-0184",
- datum: "28.07.2026",
+ iso: "2026-07-28",
status: "versendet" as const,
tracking: "00340434161094015791",
artikel: [
@@ -29,18 +32,48 @@ const beispielBestellungenRoh = [
},
{
nummer: "VDB-2026-0157",
- datum: "12.07.2026",
+ iso: "2026-07-12",
status: "abgeschlossen" as const,
tracking: null,
artikel: [{ slug: "haekelnadel-set-ergonomisch", menge: 1 }],
},
+ // Aeltere Beispielbestellungen: landen automatisch im aufklappbaren Archiv-Bereich weiter
+ // unten, damit die Liste oben kurz bleibt und man Aeltere gezielt suchen kann.
+ {
+ nummer: "VDB-2026-0102",
+ iso: "2026-05-21",
+ status: "abgeschlossen" as const,
+ tracking: null,
+ artikel: [{ slug: "haekeldecke-boho", menge: 1 }],
+ },
+ {
+ nummer: "VDB-2026-0061",
+ iso: "2026-03-14",
+ status: "abgeschlossen" as const,
+ tracking: null,
+ artikel: [{ slug: "makramee-ohrringe", menge: 2 }],
+ },
+ {
+ nummer: "VDB-2025-0488",
+ iso: "2025-12-02",
+ status: "abgeschlossen" as const,
+ tracking: null,
+ artikel: [
+ { slug: "bastel-starterset", menge: 1 },
+ { slug: "baumwollgarn-bundle", menge: 1 },
+ ],
+ },
];
+const anzeigeDatum = (iso: string) => {
+ const [jahr, monat, tag] = iso.split("-");
+ return `${tag}.${monat}.${jahr}`;
+};
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) };
+ return { ...b, datum: anzeigeDatum(b.iso), artikel, summe: artikel.reduce((s, a) => s + a.preis * a.menge, 0) };
});
const statusLabel = {
@@ -147,7 +180,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
+
+ {/* Bestellungen, die aelter als ein Monat sind, stehen nicht dauerhaft in der Liste, sondern
+ gebuendelt hinter diesem Knopf — sonst wird die Uebersicht mit den Jahren unuebersichtlich
+ lang. Aufgeklappt kann man sie gezielt nach Bestellnummer oder Artikel durchsuchen.
+ WICHTIG: Ob eine Bestellung "aelter als ein Monat" ist, wird ERST IM BROWSER berechnet
+ (siehe Skript unten). Die Seite ist statisch vorgebaut — eine schon beim Bauen berechnete
+ Grenze wuerde mit jedem Tag ungenauer werden. */}
+
+
+
+
+
+ 🔍
+
+
+
{t.accountDash.olderOrdersNoHits}
+
+
@@ -554,9 +623,97 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
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 orderCards = Array.from(document.querySelectorAll(".order-card")) as HTMLElement[];
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
+ /* ---- Aeltere Bestellungen (> 1 Monat) buendeln + durchsuchbar machen -------------------
+ Die Grenze wird hier im Browser gezogen, nicht beim Bauen der Seite: "vor einem Monat"
+ wandert taeglich mit, eine fest eingebackene Grenze waere schon am naechsten Tag falsch.
+ Gerechnet wird mit echten Kalendermonaten (setMonth(-1)), nicht mit "30 Tagen". */
+ const archivBlock = document.getElementById("order-archive");
+ const archivToggle = document.getElementById("order-archive-toggle") as HTMLButtonElement | null;
+ const archivLabel = document.getElementById("order-archive-label");
+ const archivPanel = document.getElementById("order-archive-panel");
+ const archivSuche = document.getElementById("order-archive-search") as HTMLInputElement | null;
+ const archivNoHits = document.getElementById("order-archive-nohits");
+
+ const monatsGrenze = new Date();
+ monatsGrenze.setMonth(monatsGrenze.getMonth() - 1);
+ monatsGrenze.setHours(0, 0, 0, 0);
+
+ function istAelterAlsEinMonat(card: HTMLElement) {
+ const iso = card.dataset.orderIso;
+ if (!iso) return false;
+ const d = new Date(iso + "T00:00:00");
+ return !isNaN(d.getTime()) && d < monatsGrenze;
+ }
+
+ const alteKarten = orderCards.filter(istAelterAlsEinMonat);
+ const aktuelleKarten = orderCards.filter((c) => !istAelterAlsEinMonat(c));
+
+ // Alte Karten in den Aufklapp-Bereich verschieben. Sie bleiben dabei im Dokument, also
+ // funktionieren Rechnung/Rezension/Sendungsverfolgung darin unveraendert weiter.
+ if (archivPanel) alteKarten.forEach((c) => archivPanel.appendChild(c));
+
+ let aktuellerTab: string | null = null;
+ let archivOffen = false;
+ let archivTrefferGesamt = 0;
+
+ function passtZuTab(card: HTMLElement, tab: string | null) {
+ const status = OFFENE_STATUS.find((s) => card.classList.contains("order-card-" + s));
+ return tab === "offene" ? !!status : card.classList.contains("order-card-abgeschlossen");
+ }
+
+ // Durchsucht Bestellnummer UND Artikelnamen — genau das, wonach man eine alte Bestellung
+ // typischerweise sucht ("wo war nochmal die Decke?").
+ function passtZurSuche(card: HTMLElement, begriff: string) {
+ if (!begriff) return true;
+ const nummer = card.querySelector(".order-no")?.textContent ?? "";
+ const artikel = Array.from(card.querySelectorAll(".order-items li")).map((li) => li.textContent ?? "").join(" ");
+ const datum = card.querySelector(".order-meta")?.textContent ?? "";
+ return (nummer + " " + artikel + " " + datum).toLowerCase().includes(begriff);
+ }
+
+ function aktualisiereArchiv() {
+ if (!archivBlock) return;
+ const begriff = (archivSuche?.value ?? "").trim().toLowerCase();
+
+ // Wie viele aeltere Bestellungen gehoeren ueberhaupt zum aktuellen Tab?
+ const imTab = alteKarten.filter((c) => passtZuTab(c, aktuellerTab));
+ const treffer = imTab.filter((c) => passtZurSuche(c, begriff));
+ archivTrefferGesamt = treffer.length;
+
+ // Sichtbarkeit der einzelnen alten Karten
+ alteKarten.forEach((c) => {
+ const zeigen = archivOffen && imTab.includes(c) && passtZurSuche(c, begriff);
+ c.style.display = zeigen ? "" : "none";
+ });
+
+ // Gibt es im aktuellen Tab gar keine alten Bestellungen, verschwindet der ganze Knopf.
+ archivBlock.hidden = imTab.length === 0;
+ if (archivLabel && archivToggle) {
+ archivLabel.textContent = archivOffen
+ ? (archivToggle.dataset.labelHide ?? "")
+ : `${archivToggle.dataset.labelShow ?? ""} (${imTab.length})`;
+ }
+ if (archivNoHits) archivNoHits.hidden = !(archivOffen && begriff !== "" && treffer.length === 0);
+ }
+
+ archivToggle?.addEventListener("click", () => {
+ archivOffen = !archivOffen;
+ archivToggle.setAttribute("aria-expanded", String(archivOffen));
+ archivBlock?.classList.toggle("is-open", archivOffen);
+ if (archivPanel) archivPanel.hidden = !archivOffen;
+ aktualisiereArchiv();
+ if (archivOffen) archivSuche?.focus();
+ });
+
+ archivSuche?.addEventListener("input", aktualisiereArchiv);
+ // Escape leert die Suche, statt (wie sonst bei type="search") nur das Feld zu leeren.
+ archivSuche?.addEventListener("keydown", (e) => {
+ if ((e as KeyboardEvent).key === "Escape") { archivSuche.value = ""; aktualisiereArchiv(); }
+ });
+
// 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) {
@@ -595,15 +752,19 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
}
if (bestellungenSection) bestellungenSection.style.display = "";
+ aktuellerTab = tab;
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");
+ aktuelleKarten.forEach((card) => {
+ const show = passtZuTab(card, tab);
(card as HTMLElement).style.display = show ? "" : "none";
if (show) visibleCount++;
});
+ aktualisiereArchiv();
if (emptyNote) {
- if (visibleCount === 0 && (tab === "offene" || tab === "bestellungen")) {
+ // Aeltere (archivierte) Bestellungen zaehlen hier bewusst mit: Wer im Archiv Treffer hat,
+ // soll nicht faelschlich "keine Bestellungen" lesen.
+ const gesamt = visibleCount + archivTrefferGesamt;
+ if (gesamt === 0 && (tab === "offene" || tab === "bestellungen")) {
emptyNote.textContent = tab === "offene" ? emptyNote.dataset.emptyOpen ?? "" : emptyNote.dataset.emptyArrived ?? "";
emptyNote.style.display = "block";
} else {
diff --git a/src/pages/konto/angemeldet.astro b/src/pages/konto/angemeldet.astro
index 2fd0936..2f848db 100644
--- a/src/pages/konto/angemeldet.astro
+++ b/src/pages/konto/angemeldet.astro
@@ -16,10 +16,13 @@ const t = useTranslations(lang);
// 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.
+// Das Bestelldatum steht bewusst als ISO-Datum (JJJJ-MM-TT) da: Daraus wird sowohl die Anzeige
+// abgeleitet als auch im Skript unten berechnet, welche Bestellung "aelter als ein Monat" ist.
+// So gibt es nur EINE Quelle fuer das Datum, die nicht auseinanderlaufen kann.
const beispielBestellungenRoh = [
{
nummer: "VDB-2026-0184",
- datum: "28.07.2026",
+ iso: "2026-07-28",
status: "versendet" as const,
tracking: "00340434161094015791",
artikel: [
@@ -29,18 +32,48 @@ const beispielBestellungenRoh = [
},
{
nummer: "VDB-2026-0157",
- datum: "12.07.2026",
+ iso: "2026-07-12",
status: "abgeschlossen" as const,
tracking: null,
artikel: [{ slug: "haekelnadel-set-ergonomisch", menge: 1 }],
},
+ // Aeltere Beispielbestellungen: landen automatisch im aufklappbaren Archiv-Bereich weiter
+ // unten, damit die Liste oben kurz bleibt und man Aeltere gezielt suchen kann.
+ {
+ nummer: "VDB-2026-0102",
+ iso: "2026-05-21",
+ status: "abgeschlossen" as const,
+ tracking: null,
+ artikel: [{ slug: "haekeldecke-boho", menge: 1 }],
+ },
+ {
+ nummer: "VDB-2026-0061",
+ iso: "2026-03-14",
+ status: "abgeschlossen" as const,
+ tracking: null,
+ artikel: [{ slug: "makramee-ohrringe", menge: 2 }],
+ },
+ {
+ nummer: "VDB-2025-0488",
+ iso: "2025-12-02",
+ status: "abgeschlossen" as const,
+ tracking: null,
+ artikel: [
+ { slug: "bastel-starterset", menge: 1 },
+ { slug: "baumwollgarn-bundle", menge: 1 },
+ ],
+ },
];
+const anzeigeDatum = (iso: string) => {
+ const [jahr, monat, tag] = iso.split("-");
+ return `${tag}.${monat}.${jahr}`;
+};
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) };
+ return { ...b, datum: anzeigeDatum(b.iso), artikel, summe: artikel.reduce((s, a) => s + a.preis * a.menge, 0) };
});
const statusLabel = {
@@ -148,7 +181,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
+
+ {/* Bestellungen, die aelter als ein Monat sind, stehen nicht dauerhaft in der Liste, sondern
+ gebuendelt hinter diesem Knopf — sonst wird die Uebersicht mit den Jahren unuebersichtlich
+ lang. Aufgeklappt kann man sie gezielt nach Bestellnummer oder Artikel durchsuchen.
+ WICHTIG: Ob eine Bestellung "aelter als ein Monat" ist, wird ERST IM BROWSER berechnet
+ (siehe Skript unten). Die Seite ist statisch vorgebaut — eine schon beim Bauen berechnete
+ Grenze wuerde mit jedem Tag ungenauer werden. */}
+
+
+
+
+
+ 🔍
+
+
+
{t.accountDash.olderOrdersNoHits}
+
+
@@ -563,9 +632,97 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
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 orderCards = Array.from(document.querySelectorAll(".order-card")) as HTMLElement[];
const emptyNote = document.getElementById("orders-empty-note") as HTMLElement | null;
+ /* ---- Aeltere Bestellungen (> 1 Monat) buendeln + durchsuchbar machen -------------------
+ Die Grenze wird hier im Browser gezogen, nicht beim Bauen der Seite: "vor einem Monat"
+ wandert taeglich mit, eine fest eingebackene Grenze waere schon am naechsten Tag falsch.
+ Gerechnet wird mit echten Kalendermonaten (setMonth(-1)), nicht mit "30 Tagen". */
+ const archivBlock = document.getElementById("order-archive");
+ const archivToggle = document.getElementById("order-archive-toggle") as HTMLButtonElement | null;
+ const archivLabel = document.getElementById("order-archive-label");
+ const archivPanel = document.getElementById("order-archive-panel");
+ const archivSuche = document.getElementById("order-archive-search") as HTMLInputElement | null;
+ const archivNoHits = document.getElementById("order-archive-nohits");
+
+ const monatsGrenze = new Date();
+ monatsGrenze.setMonth(monatsGrenze.getMonth() - 1);
+ monatsGrenze.setHours(0, 0, 0, 0);
+
+ function istAelterAlsEinMonat(card: HTMLElement) {
+ const iso = card.dataset.orderIso;
+ if (!iso) return false;
+ const d = new Date(iso + "T00:00:00");
+ return !isNaN(d.getTime()) && d < monatsGrenze;
+ }
+
+ const alteKarten = orderCards.filter(istAelterAlsEinMonat);
+ const aktuelleKarten = orderCards.filter((c) => !istAelterAlsEinMonat(c));
+
+ // Alte Karten in den Aufklapp-Bereich verschieben. Sie bleiben dabei im Dokument, also
+ // funktionieren Rechnung/Rezension/Sendungsverfolgung darin unveraendert weiter.
+ if (archivPanel) alteKarten.forEach((c) => archivPanel.appendChild(c));
+
+ let aktuellerTab: string | null = null;
+ let archivOffen = false;
+ let archivTrefferGesamt = 0;
+
+ function passtZuTab(card: HTMLElement, tab: string | null) {
+ const status = OFFENE_STATUS.find((s) => card.classList.contains("order-card-" + s));
+ return tab === "offene" ? !!status : card.classList.contains("order-card-abgeschlossen");
+ }
+
+ // Durchsucht Bestellnummer UND Artikelnamen — genau das, wonach man eine alte Bestellung
+ // typischerweise sucht ("wo war nochmal die Decke?").
+ function passtZurSuche(card: HTMLElement, begriff: string) {
+ if (!begriff) return true;
+ const nummer = card.querySelector(".order-no")?.textContent ?? "";
+ const artikel = Array.from(card.querySelectorAll(".order-items li")).map((li) => li.textContent ?? "").join(" ");
+ const datum = card.querySelector(".order-meta")?.textContent ?? "";
+ return (nummer + " " + artikel + " " + datum).toLowerCase().includes(begriff);
+ }
+
+ function aktualisiereArchiv() {
+ if (!archivBlock) return;
+ const begriff = (archivSuche?.value ?? "").trim().toLowerCase();
+
+ // Wie viele aeltere Bestellungen gehoeren ueberhaupt zum aktuellen Tab?
+ const imTab = alteKarten.filter((c) => passtZuTab(c, aktuellerTab));
+ const treffer = imTab.filter((c) => passtZurSuche(c, begriff));
+ archivTrefferGesamt = treffer.length;
+
+ // Sichtbarkeit der einzelnen alten Karten
+ alteKarten.forEach((c) => {
+ const zeigen = archivOffen && imTab.includes(c) && passtZurSuche(c, begriff);
+ c.style.display = zeigen ? "" : "none";
+ });
+
+ // Gibt es im aktuellen Tab gar keine alten Bestellungen, verschwindet der ganze Knopf.
+ archivBlock.hidden = imTab.length === 0;
+ if (archivLabel && archivToggle) {
+ archivLabel.textContent = archivOffen
+ ? (archivToggle.dataset.labelHide ?? "")
+ : `${archivToggle.dataset.labelShow ?? ""} (${imTab.length})`;
+ }
+ if (archivNoHits) archivNoHits.hidden = !(archivOffen && begriff !== "" && treffer.length === 0);
+ }
+
+ archivToggle?.addEventListener("click", () => {
+ archivOffen = !archivOffen;
+ archivToggle.setAttribute("aria-expanded", String(archivOffen));
+ archivBlock?.classList.toggle("is-open", archivOffen);
+ if (archivPanel) archivPanel.hidden = !archivOffen;
+ aktualisiereArchiv();
+ if (archivOffen) archivSuche?.focus();
+ });
+
+ archivSuche?.addEventListener("input", aktualisiereArchiv);
+ // Escape leert die Suche, statt (wie sonst bei type="search") nur das Feld zu leeren.
+ archivSuche?.addEventListener("keydown", (e) => {
+ if ((e as KeyboardEvent).key === "Escape") { archivSuche.value = ""; aktualisiereArchiv(); }
+ });
+
// 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) {
@@ -604,15 +761,19 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
}
if (bestellungenSection) bestellungenSection.style.display = "";
+ aktuellerTab = tab;
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");
+ aktuelleKarten.forEach((card) => {
+ const show = passtZuTab(card, tab);
(card as HTMLElement).style.display = show ? "" : "none";
if (show) visibleCount++;
});
+ aktualisiereArchiv();
if (emptyNote) {
- if (visibleCount === 0 && (tab === "offene" || tab === "bestellungen")) {
+ // Aeltere (archivierte) Bestellungen zaehlen hier bewusst mit: Wer im Archiv Treffer hat,
+ // soll nicht faelschlich "keine Bestellungen" lesen.
+ const gesamt = visibleCount + archivTrefferGesamt;
+ if (gesamt === 0 && (tab === "offene" || tab === "bestellungen")) {
emptyNote.textContent = tab === "offene" ? emptyNote.dataset.emptyOpen ?? "" : emptyNote.dataset.emptyArrived ?? "";
emptyNote.style.display = "block";
} else {
diff --git a/src/scripts/account.ts b/src/scripts/account.ts
index f213739..c1debef 100644
--- a/src/scripts/account.ts
+++ b/src/scripts/account.ts
@@ -150,7 +150,21 @@ function leeresKonto(): Account {
export function getAccount(): Account {
if (typeof localStorage === "undefined") return leeresKonto();
try {
- return { ...leeresKonto(), ...JSON.parse(localStorage.getItem(KEY) || "{}") };
+ const basis = leeresKonto();
+ const gespeichert = JSON.parse(localStorage.getItem(KEY) || "{}") ?? {};
+ // Bewusst KEIN rein flaches Zusammenführen: Steht im Speicher ein fehlendes oder kaputtes
+ // abo-Objekt (z.B. null aus einer älteren Fassung oder nach einem abgebrochenen Schreiben),
+ // würde das den Standardwert überschreiben — und jeder Zugriff wie a.abo.bezahltBisDatum
+ // wirft dann einen Fehler, der das GESAMTE Konto-Dashboard lahmlegt (keine Bestellungen,
+ // keine Tabs mehr). Deshalb wird das verschachtelte abo-Objekt ausdrücklich mit den
+ // Standardwerten aufgefüllt.
+ return {
+ ...basis,
+ ...gespeichert,
+ abo: { ...basis.abo, ...(gespeichert.abo ?? {}) },
+ bewerteteBestellungen: gespeichert.bewerteteBestellungen ?? basis.bewerteteBestellungen,
+ benachrichtigungen: gespeichert.benachrichtigungen ?? basis.benachrichtigungen,
+ };
} catch {
return leeresKonto();
}
diff --git a/src/styles/global.css b/src/styles/global.css
index 840f2e6..55b7718 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1683,6 +1683,54 @@ a:focus-visible, button:focus-visible {
}
.order-list { display: flex; flex-direction: column; gap: 1.1rem; }
+
+/* Aufklappbereich für Bestellungen, die älter als ein Monat sind (siehe konto/angemeldet.astro).
+ Bewusst zurückhaltend gestaltet: Es ist ein Archiv-Zugang, kein Hauptinhalt — soll die
+ aktuellen Bestellungen darüber nicht optisch überstrahlen. */
+.order-archive { margin-top: 1.2rem; display: flex; flex-direction: column; gap: 0.9rem; }
+.order-archive[hidden] { display: none; }
+.order-archive-toggle {
+ align-self: flex-start;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.55em;
+ padding: 0.5em 1em;
+ border-radius: 999px;
+ border: 1px dashed rgba(244, 241, 247, 0.22);
+ background: transparent;
+ color: var(--c-text-muted);
+ font-family: inherit;
+ font-size: 0.88rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: border-color 0.2s var(--ease), color 0.2s var(--ease), background 0.2s var(--ease);
+}
+.order-archive-toggle:hover {
+ border-color: var(--c-accent);
+ border-style: solid;
+ color: var(--c-text);
+ background: color-mix(in srgb, var(--c-accent) 10%, transparent);
+}
+.order-archive-icon { font-size: 1em; }
+.order-archive-chevron { font-size: 0.7em; transition: transform 0.2s var(--ease); }
+.order-archive.is-open .order-archive-chevron { transform: rotate(180deg); }
+.order-archive-panel { display: flex; flex-direction: column; gap: 1.1rem; }
+.order-archive-panel[hidden] { display: none; }
+.order-archive-search { position: relative; display: flex; align-items: center; }
+.order-archive-search-icon { position: absolute; left: 0.85em; font-size: 0.9rem; opacity: 0.6; pointer-events: none; }
+.order-archive-search input {
+ width: 100%;
+ padding: 0.6em 0.9em 0.6em 2.4em;
+ border-radius: var(--radius-m);
+ border: 1px solid rgba(244, 241, 247, 0.14);
+ background: var(--c-anthracite-light);
+ color: var(--c-text);
+ font-family: inherit;
+ font-size: 0.92rem;
+}
+.order-archive-search input:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 1px; }
+.order-archive-nohits { color: var(--c-text-muted); margin: 0; }
+.order-archive-nohits[hidden] { display: none; }
.order-card { display: flex; flex-direction: column; gap: 0.9rem; }
.order-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.order-no { display: block; font-weight: 700; font-size: 1.02rem; }