Aeltere Bestellungen (> 1 Monat) hinter Aufklapp-Knopf mit Suche

Im Kundenbereich stehen jetzt nur noch Bestellungen aus dem letzten Monat direkt in der Liste.
Aeltere werden gebuendelt hinter einem kleinen Knopf "Aeltere Bestellungen (N)" gezeigt und
lassen sich dort nach Bestellnummer, Artikel oder Datum durchsuchen. Alle 4 Sprachen.

Die Monatsgrenze wird bewusst ERST IM BROWSER berechnet (echte Kalendermonate, nicht "30 Tage"):
Die Seite ist statisch vorgebaut, eine schon beim Bauen gezogene Grenze waere ab dem naechsten
Tag falsch. Bestelldatum steht dafuer jetzt als ISO-Datum in den Daten, die Anzeige wird daraus
abgeleitet — nur noch eine Quelle fuers Datum.

Nebenbei gefunden und behoben: getAccount() hat gespeicherte Kontodaten reind flach mit den
Standardwerten zusammengefuehrt. Stand im Speicher ein kaputtes/fehlendes abo-Objekt (null),
hat das den Standardwert ueberschrieben und jeder Zugriff wie a.abo.bezahltBisDatum warf einen
Fehler, der das GESAMTE Konto-Dashboard lahmgelegt hat (keine Bestellungen, keine Tabs mehr).
Das verschachtelte abo-Objekt wird jetzt ausdruecklich mit Standardwerten aufgefuellt.

Getestet (Browser, alle Faelle): Aufteilung nach Datum, Auf-/Zuklappen, Suche nach Artikel und
nach Bestellnummer, Suche ohne Treffer, Zusammenspiel mit den Tabs "Offene Sendungen"/
"Bestellungen", sowie Markup und Uebersetzungen in allen 4 Sprachen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-04 10:12:56 +02:00
co-authored by Claude Opus 5
parent 7f3b77a036
commit be56c78c4f
7 changed files with 759 additions and 37 deletions
+16
View File
@@ -296,6 +296,10 @@ export const ui = {
notifAboTeddyGrossEingeloest: "🧸 Großer Teddy eingelöst — er liegt jetzt kostenlos in deinem Warenkorb.",
noOpenOrders: "Aktuell keine offenen Sendungen — alle Bestellungen sind schon angekommen. 🎉",
noArrivedOrders: "Noch keine angekommenen Bestellungen — hier erscheinen deine Bestellungen, sobald sie geliefert wurden.",
olderOrdersShow: "Ältere Bestellungen",
olderOrdersHide: "Ältere Bestellungen ausblenden",
olderOrdersSearch: "Bestellnummer oder Artikel suchen …",
olderOrdersNoHits: "Dazu haben wir keine ältere Bestellung gefunden.",
ordersTitle: "📦 Meine Bestellungen",
orderNo: "Bestellung", orderDate: "Bestellt am", orderTotal: "Gesamt", orderItems: "Artikel",
trackingLabel: "Sendungsverfolgung", trackingBtn: "Sendung verfolgen →", invoiceBtn: "Rechnung ansehen", reorderBtn: "Nochmal bestellen",
@@ -603,6 +607,10 @@ export const ui = {
notifAboTeddyGrossEingeloest: "🧸 Large teddy redeemed — it's now in your cart for free.",
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.",
olderOrdersShow: "Older orders",
olderOrdersHide: "Hide older orders",
olderOrdersSearch: "Search order number or item …",
olderOrdersNoHits: "We couldn't find an older order for that.",
ordersTitle: "📦 My orders",
orderNo: "Order", orderDate: "Ordered on", orderTotal: "Total", orderItems: "Items",
trackingLabel: "Tracking", trackingBtn: "Track shipment →", invoiceBtn: "View invoice", reorderBtn: "Order again",
@@ -910,6 +918,10 @@ export const ui = {
notifAboTeddyGrossEingeloest: "🧸 Grosse Teddy ilöst — er liit jetzt gratis in dim Warechorb.",
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.",
olderOrdersShow: "Elteri Bstellige",
olderOrdersHide: "Elteri Bstellige uusblende",
olderOrdersSearch: "Bstellnummere oder Artikel sueche …",
olderOrdersNoHits: "Dezue hemmer kei elteri Bstellig gfunde.",
ordersTitle: "📦 Mini Bstellige",
orderNo: "Bstellig", orderDate: "Bstellt am", orderTotal: "Total", orderItems: "Artikel",
trackingLabel: "Sändigsverfolgig", trackingBtn: "Sändig verfolge →", invoiceBtn: "Rächnig aluege", reorderBtn: "Nomal bstelle",
@@ -1217,6 +1229,10 @@ export const ui = {
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. 🎉",
noArrivedOrders: "Aucune commande livrée pour le moment — vos commandes apparaîtront ici dès qu'elles seront arrivées.",
olderOrdersShow: "Commandes plus anciennes",
olderOrdersHide: "Masquer les commandes plus anciennes",
olderOrdersSearch: "Rechercher un numéro de commande ou un article …",
olderOrdersNoHits: "Nous n'avons trouvé aucune commande plus ancienne pour cette recherche.",
ordersTitle: "📦 Mes commandes",
orderNo: "Commande", orderDate: "Commandé le", orderTotal: "Total", orderItems: "Articles",
trackingLabel: "Suivi", trackingBtn: "Suivre l'envoi →", invoiceBtn: "Voir la facture", reorderBtn: "Commander à nouveau",
+170 -9
View File
@@ -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 ===
<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={`card order-card order-card-${b.status}`} data-order-iso={b.iso}>
<div class="order-card-head">
<div>
<span class="order-no">{t.accountDash.orderNo} {b.nummer}</span>
@@ -255,6 +288,42 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
))}
</div>
{/* 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. */}
<div class="order-archive" id="order-archive" hidden>
<button
type="button"
class="order-archive-toggle"
id="order-archive-toggle"
aria-expanded="false"
aria-controls="order-archive-panel"
data-label-show={t.accountDash.olderOrdersShow}
data-label-hide={t.accountDash.olderOrdersHide}
>
<span class="order-archive-icon" aria-hidden="true">📦</span>
<span class="order-archive-label" id="order-archive-label"></span>
<span class="order-archive-chevron" aria-hidden="true">▾</span>
</button>
<div class="order-archive-panel" id="order-archive-panel" hidden>
<div class="order-archive-search">
<span class="order-archive-search-icon" aria-hidden="true">🔍</span>
<input
type="search"
id="order-archive-search"
autocomplete="off"
placeholder={t.accountDash.olderOrdersSearch}
aria-label={t.accountDash.olderOrdersSearch}
/>
</div>
<p class="small order-archive-nohits" id="order-archive-nohits" hidden>{t.accountDash.olderOrdersNoHits}</p>
</div>
</div>
</div>
</section>
@@ -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 {
+170 -9
View File
@@ -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 ===
<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={`card order-card order-card-${b.status}`} data-order-iso={b.iso}>
<div class="order-card-head">
<div>
<span class="order-no">{t.accountDash.orderNo} {b.nummer}</span>
@@ -255,6 +288,42 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
))}
</div>
{/* 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. */}
<div class="order-archive" id="order-archive" hidden>
<button
type="button"
class="order-archive-toggle"
id="order-archive-toggle"
aria-expanded="false"
aria-controls="order-archive-panel"
data-label-show={t.accountDash.olderOrdersShow}
data-label-hide={t.accountDash.olderOrdersHide}
>
<span class="order-archive-icon" aria-hidden="true">📦</span>
<span class="order-archive-label" id="order-archive-label"></span>
<span class="order-archive-chevron" aria-hidden="true">▾</span>
</button>
<div class="order-archive-panel" id="order-archive-panel" hidden>
<div class="order-archive-search">
<span class="order-archive-search-icon" aria-hidden="true">🔍</span>
<input
type="search"
id="order-archive-search"
autocomplete="off"
placeholder={t.accountDash.olderOrdersSearch}
aria-label={t.accountDash.olderOrdersSearch}
/>
</div>
<p class="small order-archive-nohits" id="order-archive-nohits" hidden>{t.accountDash.olderOrdersNoHits}</p>
</div>
</div>
</div>
</section>
@@ -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 {
+170 -9
View File
@@ -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 ===
<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={`card order-card order-card-${b.status}`} data-order-iso={b.iso}>
<div class="order-card-head">
<div>
<span class="order-no">{t.accountDash.orderNo} {b.nummer}</span>
@@ -255,6 +288,42 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
))}
</div>
{/* 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. */}
<div class="order-archive" id="order-archive" hidden>
<button
type="button"
class="order-archive-toggle"
id="order-archive-toggle"
aria-expanded="false"
aria-controls="order-archive-panel"
data-label-show={t.accountDash.olderOrdersShow}
data-label-hide={t.accountDash.olderOrdersHide}
>
<span class="order-archive-icon" aria-hidden="true">📦</span>
<span class="order-archive-label" id="order-archive-label"></span>
<span class="order-archive-chevron" aria-hidden="true">▾</span>
</button>
<div class="order-archive-panel" id="order-archive-panel" hidden>
<div class="order-archive-search">
<span class="order-archive-search-icon" aria-hidden="true">🔍</span>
<input
type="search"
id="order-archive-search"
autocomplete="off"
placeholder={t.accountDash.olderOrdersSearch}
aria-label={t.accountDash.olderOrdersSearch}
/>
</div>
<p class="small order-archive-nohits" id="order-archive-nohits" hidden>{t.accountDash.olderOrdersNoHits}</p>
</div>
</div>
</div>
</section>
@@ -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 {
+170 -9
View File
@@ -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 ===
<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={`card order-card order-card-${b.status}`} data-order-iso={b.iso}>
<div class="order-card-head">
<div>
<span class="order-no">{t.accountDash.orderNo} {b.nummer}</span>
@@ -256,6 +289,42 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status ===
</div>
))}
</div>
{/* 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. */}
<div class="order-archive" id="order-archive" hidden>
<button
type="button"
class="order-archive-toggle"
id="order-archive-toggle"
aria-expanded="false"
aria-controls="order-archive-panel"
data-label-show={t.accountDash.olderOrdersShow}
data-label-hide={t.accountDash.olderOrdersHide}
>
<span class="order-archive-icon" aria-hidden="true">📦</span>
<span class="order-archive-label" id="order-archive-label"></span>
<span class="order-archive-chevron" aria-hidden="true">▾</span>
</button>
<div class="order-archive-panel" id="order-archive-panel" hidden>
<div class="order-archive-search">
<span class="order-archive-search-icon" aria-hidden="true">🔍</span>
<input
type="search"
id="order-archive-search"
autocomplete="off"
placeholder={t.accountDash.olderOrdersSearch}
aria-label={t.accountDash.olderOrdersSearch}
/>
</div>
<p class="small order-archive-nohits" id="order-archive-nohits" hidden>{t.accountDash.olderOrdersNoHits}</p>
</div>
</div>
</div>
</section>
@@ -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 {
+15 -1
View File
@@ -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();
}
+48
View File
@@ -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; }