From be56c78c4fdf2762ace3974804cc358dfb4a7ccd Mon Sep 17 00:00:00 2001 From: qcigano Date: Tue, 4 Aug 2026 10:12:56 +0200 Subject: [PATCH] Aeltere Bestellungen (> 1 Monat) hinter Aufklapp-Knopf mit Suche MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/i18n/ui.ts | 16 +++ src/pages/ch/konto/angemeldet.astro | 179 ++++++++++++++++++++++++++-- src/pages/en/konto/angemeldet.astro | 179 ++++++++++++++++++++++++++-- src/pages/fr/konto/angemeldet.astro | 179 ++++++++++++++++++++++++++-- src/pages/konto/angemeldet.astro | 179 ++++++++++++++++++++++++++-- src/scripts/account.ts | 16 ++- src/styles/global.css | 48 ++++++++ 7 files changed, 759 insertions(+), 37 deletions(-) diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index e409c75..aed6bbf 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -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", diff --git a/src/pages/ch/konto/angemeldet.astro b/src/pages/ch/konto/angemeldet.astro index a700bce..3b4b3ed 100644 --- a/src/pages/ch/konto/angemeldet.astro +++ b/src/pages/ch/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 ===
{beispielBestellungen.map((b) => ( -
+
{t.accountDash.orderNo} {b.nummer} @@ -255,6 +288,42 @@ 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. */} +
@@ -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 ===
{beispielBestellungen.map((b) => ( -
+
{t.accountDash.orderNo} {b.nummer} @@ -255,6 +288,42 @@ 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. */} +
@@ -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 ===
{beispielBestellungen.map((b) => ( -
+
{t.accountDash.orderNo} {b.nummer} @@ -255,6 +288,42 @@ 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. */} +
@@ -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 ===
{beispielBestellungen.map((b) => ( -
+
{t.accountDash.orderNo} {b.nummer} @@ -256,6 +289,42 @@ 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. */} +
@@ -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; }