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; }