diff --git a/src/pages/verwaltung/index.astro b/src/pages/verwaltung/index.astro index d1b596f..382f651 100644 --- a/src/pages/verwaltung/index.astro +++ b/src/pages/verwaltung/index.astro @@ -158,7 +158,10 @@ const lang: Locale = "de";

Noch keine ausgewerteten Verkäufe.

Hier siehst du bald, welche Produkte je Kategorie am besten laufen (stornierte Bestellungen zählen nicht mit).

-
+ {/* Jede Kategorie ist jetzt eine eigene Kachel im selben Stil wie die + Bestellstatus-Kacheln oben — Klick öffnet direkt darunter die volle Produktliste. */} +
+ @@ -173,7 +176,8 @@ const lang: Locale = "de";

Noch keine Bestellungen.

Sobald Bestellungen eingehen, siehst du hier den Anteil je Land (DE/AT/CH/LU).

-
+
+ @@ -188,7 +192,8 @@ const lang: Locale = "de";

Noch keine wiederkehrende Kundschaft.

Sobald jemand zum zweiten Mal bestellt, erscheint sie hier — sortiert nach bisherigem Umsatz.

-
+
+ @@ -296,6 +301,12 @@ const lang: Locale = "de"; let alleBestellungen = []; let bekannteIds = new Set(); let erstesLaden = true; + // Merkt sich die zuletzt berechneten Auswertungsdaten, damit die Detail-Panels (Klick auf eine + // Kategorie-/Land-/Kundschafts-Kachel) darauf zugreifen können, ohne alles neu zu berechnen. + let letzteKategorien = []; + let letzteLaender = []; + let letzteKunden = []; + let letzteAktive = []; async function ladeUndRendere() { loadingEl.style.display = ""; @@ -359,6 +370,15 @@ const lang: Locale = "de"; function render() { const aktive = alleBestellungen.filter((b) => b.status !== "storniert"); + letzteAktive = aktive; + + // Die Auswertungs-Detail-Panels zeigen ggf. veraltete Daten, sobald sich die Bestellliste + // ändert (z.B. neuer Status) — bei jedem Rendern lieber wieder einklappen, statt falsche + // Zahlen stehen zu lassen. Ein erneuter Klick zeigt die aktuellen Daten sofort wieder an. + ["kat", "land", "kunden"].forEach((gruppe) => { + document.getElementById(`verwaltung-${gruppe}-detail`)?.setAttribute("hidden", ""); + }); + document.querySelectorAll(".verwaltung-stat-tile.is-active").forEach((t) => t.classList.remove("is-active")); // ── Buckets für die Stat-Kacheln + Filterleiste ── const buckets = { offen: 0, inBearbeitung: 0, versendet: 0, abgeschlossen: 0, storniert: 0 }; @@ -502,10 +522,13 @@ const lang: Locale = "de"; name: kat?.name?.[lang] ?? kategorieSlug, icon: kat?.icon ?? "📦", gesamtMenge: sortiert.reduce((s, p) => s + p.menge, 0), - produkte: sortiert.slice(0, 4).map((p) => ({ ...p, prozent: Math.round((p.menge / maxMenge) * 100) })), + // Volle Liste (nicht mehr auf 4 gekürzt) — die Kachel selbst zeigt nur die Summe, die + // vollständige Rangliste erscheint erst im aufklappbaren Detail-Panel darunter. + produkte: sortiert.map((p) => ({ ...p, prozent: Math.round((p.menge / maxMenge) * 100) })), }; }) .sort((a, b) => b.gesamtMenge - a.gesamtMenge); + letzteKategorien = auswertungKategorien; const katGrid = document.getElementById("verwaltung-kat-grid"); const katEmpty = document.getElementById("verwaltung-kat-empty"); @@ -515,17 +538,11 @@ const lang: Locale = "de"; } else { katEmpty.style.display = "none"; katGrid.innerHTML = auswertungKategorien.map((k) => ` -
-

${k.icon} ${escapeHtml(k.name)}

-
- ${k.produkte.map((p) => ` -
-
${escapeHtml(p.name)}${p.menge}× verkauft
-
-
- `).join("")} -
-
+ `).join(""); } @@ -540,20 +557,22 @@ const lang: Locale = "de"; count, prozent: aktive.length > 0 ? Math.round((count / aktive.length) * 100) : 0, balken: Math.round((count / maxLandCount) * 100), })); + letzteLaender = auswertungLaender; - const landList = document.getElementById("verwaltung-land-list"); + const landGrid = document.getElementById("verwaltung-land-grid"); const landEmpty = document.getElementById("verwaltung-land-empty"); if (auswertungLaender.length === 0) { - landList.innerHTML = ""; + landGrid.innerHTML = ""; landEmpty.style.display = ""; } else { landEmpty.style.display = "none"; - landList.innerHTML = auswertungLaender.map((l) => ` -
-
${l.flagge} ${escapeHtml(l.name)}${l.count} Bestellungen · ${l.prozent}%
-
-
- `).join(""); + landGrid.innerHTML = auswertungLaender.map((l) => ` + + `).join(""); } // ── NEU: Beste Kundinnen (Umsatz pro E-Mail, nur wer mehr als 1× bestellt hat) ── @@ -569,25 +588,93 @@ const lang: Locale = "de"; .map(([email, v]) => ({ email, ...v })) .sort((a, b) => b.summe - a.summe) .slice(0, 8); - const maxKundenUmsatz = Math.max(1, ...besteKunden.map((k) => k.summe)); + letzteKunden = besteKunden; const kundenEmpty = document.getElementById("verwaltung-kunden-empty"); - const kundenList = document.getElementById("verwaltung-kunden-list"); + const kundenGrid = document.getElementById("verwaltung-kunden-grid"); if (besteKunden.length === 0) { - kundenList.innerHTML = ""; + kundenGrid.innerHTML = ""; kundenEmpty.style.display = ""; } else { kundenEmpty.style.display = "none"; - kundenList.innerHTML = besteKunden.map((k) => ` -
-
👑 ${escapeHtml(k.name)} (${escapeHtml(k.email)})${formatPrice(k.summe, lang)} · ${k.anzahl}× bestellt
-
-
+ kundenGrid.innerHTML = besteKunden.map((k) => ` + `).join(""); } wendeFilterAn(); } + // ── Detail-Panels für die Auswertungs-Kacheln: erscheinen genau unter der jeweiligen Kachel- + // Reihe (siehe .verwaltung-analyse-detail), sobald man draufklickt — mit der VOLLSTÄNDIGEN + // Liste (nicht mehr nur den Top 4/8 wie vorher direkt auf der Kachel). ── + function zeigeKatDetail(slug) { + const k = letzteKategorien.find((x) => x.slug === slug); + const panel = document.getElementById("verwaltung-kat-detail"); + if (!k) { panel.setAttribute("hidden", ""); return; } + panel.innerHTML = ` +
+

${k.icon} ${escapeHtml(k.name)} — ${k.produkte.length} Produkt${k.produkte.length === 1 ? "" : "e"}

+ +
+
+ ${k.produkte.map((p) => ` +
+
${escapeHtml(p.name)}${p.menge}× verkauft
+
+
+ `).join("")} +
+ `; + panel.removeAttribute("hidden"); + document.querySelectorAll("[data-kat-tile]").forEach((t) => t.classList.toggle("is-active", t.dataset.katTile === slug)); + } + + function zeigeLandDetail(land) { + const l = letzteLaender.find((x) => x.land === land); + const panel = document.getElementById("verwaltung-land-detail"); + if (!l) { panel.setAttribute("hidden", ""); return; } + const bestellungen = letzteAktive.filter((b) => b.land === land).sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + panel.innerHTML = ` +
+

${l.flagge} ${escapeHtml(l.name)} — ${bestellungen.length} Bestellung${bestellungen.length === 1 ? "" : "en"}

+ +
+ ${bestellungen.map((b) => ` +
+ ${escapeHtml(b.bestellnummer || `#${b.id}`)} · ${escapeHtml(b.kunde_name)} + ${new Date(b.created_at).toLocaleDateString("de-DE")} · ${formatPrice(Number(b.summe), lang)} +
+ `).join("")} + `; + panel.removeAttribute("hidden"); + document.querySelectorAll("[data-land-tile]").forEach((t) => t.classList.toggle("is-active", t.dataset.landTile === land)); + } + + function zeigeKundeDetail(email) { + const k = letzteKunden.find((x) => x.email === email); + const panel = document.getElementById("verwaltung-kunden-detail"); + if (!k) { panel.setAttribute("hidden", ""); return; } + const bestellungen = letzteAktive.filter((b) => b.kunde_email === email).sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + panel.innerHTML = ` +
+

👑 ${escapeHtml(k.name)} (${escapeHtml(email)}) — ${bestellungen.length} Bestellung${bestellungen.length === 1 ? "" : "en"}

+ +
+ ${bestellungen.map((b) => ` +
+ ${escapeHtml(b.bestellnummer || `#${b.id}`)} + ${new Date(b.created_at).toLocaleDateString("de-DE")} · ${formatPrice(Number(b.summe), lang)} +
+ `).join("")} + `; + panel.removeAttribute("hidden"); + document.querySelectorAll("[data-kunden-tile]").forEach((t) => t.classList.toggle("is-active", t.dataset.kundenTile === email)); + } + function escapeHtml(str) { return String(str ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } @@ -716,6 +803,50 @@ const lang: Locale = "de"; zeitraumSelect.value = zeitraumBtn.dataset.zeitraum; wendeFilterAn(); document.getElementById("verwaltung-order-list")?.scrollIntoView({ behavior: "smooth", block: "start" }); + return; + } + + // ── Auswertungs-Kacheln: Klick öffnet das Detail-Panel direkt darunter, nochmal draufklicken + // klappt es wieder ein. ── + const katTile = e.target.closest("[data-kat-tile]"); + if (katTile) { + const schonOffen = katTile.classList.contains("is-active"); + if (schonOffen) { + document.getElementById("verwaltung-kat-detail").setAttribute("hidden", ""); + katTile.classList.remove("is-active"); + } else { + zeigeKatDetail(katTile.dataset.katTile); + } + return; + } + const landTile = e.target.closest("[data-land-tile]"); + if (landTile) { + const schonOffen = landTile.classList.contains("is-active"); + if (schonOffen) { + document.getElementById("verwaltung-land-detail").setAttribute("hidden", ""); + landTile.classList.remove("is-active"); + } else { + zeigeLandDetail(landTile.dataset.landTile); + } + return; + } + const kundenTile = e.target.closest("[data-kunden-tile]"); + if (kundenTile) { + const schonOffen = kundenTile.classList.contains("is-active"); + if (schonOffen) { + document.getElementById("verwaltung-kunden-detail").setAttribute("hidden", ""); + kundenTile.classList.remove("is-active"); + } else { + zeigeKundeDetail(kundenTile.dataset.kundenTile); + } + return; + } + const closeDetailBtn = e.target.closest("[data-close-detail]"); + if (closeDetailBtn) { + const gruppe = closeDetailBtn.dataset.closeDetail; + document.getElementById(`verwaltung-${gruppe}-detail`)?.setAttribute("hidden", ""); + document.querySelectorAll(`[data-${gruppe}-tile]`).forEach((t) => t.classList.remove("is-active")); + return; } }); diff --git a/src/styles/global.css b/src/styles/global.css index b061765..60a7ba9 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -3034,6 +3034,55 @@ a:focus-visible, button:focus-visible { .verwaltung-auswertung-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; margin-top: 1.2rem; } .verwaltung-kat-card { display: flex; flex-direction: column; gap: 0.9rem; } .verwaltung-kat-card h3 { margin: 0; } + +/* Auswertungs-Kacheln (Bestseller je Kategorie / Herkunft / Stammkundschaft) — sehen jetzt + bewusst genauso aus wie die Bestellstatus-Kacheln oben (.verwaltung-stat-tile), auf Wunsch + ("sollen aussehen wie auf screen2"), statt eigener Bar-Listen direkt in der Kiste. Klick auf + eine Kachel öffnet direkt darunter (".verwaltung-analyse-detail") die vollständige Liste. */ +.verwaltung-analyse-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0.9rem; margin: 1.2rem 0; } +.verwaltung-stat-tile.is-active { + border-color: color-mix(in srgb, var(--box-color, var(--c-accent)) 70%, transparent); + background: color-mix(in srgb, var(--box-color, var(--c-accent)) 14%, var(--c-anthracite-light)); +} +.verwaltung-analyse-detail { + margin-top: 0.4rem; + padding: 1.2rem 1.3rem; + border-radius: var(--radius-m); + border: 1.5px solid color-mix(in srgb, var(--box-color, var(--c-accent)) 40%, transparent); + background: color-mix(in srgb, var(--box-color, var(--c-accent)) 6%, var(--c-anthracite)); + animation: verwaltung-analyse-detail-in 0.2s var(--ease); +} +.verwaltung-analyse-detail[hidden] { display: none; } +@keyframes verwaltung-analyse-detail-in { + from { opacity: 0; transform: translateY(-6px); } + to { opacity: 1; transform: translateY(0); } +} +.verwaltung-analyse-detail-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 0.9rem; } +.verwaltung-analyse-detail-head h3 { margin: 0; font-size: 1.05rem; } +.verwaltung-analyse-close { + flex-shrink: 0; + background: transparent; + border: none; + color: var(--c-text-muted); + font-size: 1.2rem; + line-height: 1; + padding: 0.2rem 0.4rem; + cursor: pointer; + transition: color 0.15s var(--ease); +} +.verwaltung-analyse-close:hover { color: var(--c-text); } +.verwaltung-analyse-order-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 0.6rem 0; + border-bottom: 1px solid rgba(244, 241, 247, 0.08); + font-size: 0.9rem; +} +.verwaltung-analyse-order-row:last-child { border-bottom: none; } +@media (prefers-reduced-motion: reduce) { .verwaltung-analyse-detail { animation: none; } } + .verwaltung-bar-list { display: flex; flex-direction: column; gap: 0.8rem; } .verwaltung-bar-head { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.9rem; margin-bottom: 0.3rem; } .verwaltung-bar-track { height: 9px; border-radius: 999px; background: rgba(244, 241, 247, 0.08); overflow: hidden; }