Verwaltung: Auswertungs-Kacheln (Bestseller/Herkunft/Stammkundschaft) im Stil der Bestellstatus-Kacheln
Jede Kategorie/jedes Land/jede Stammkundin ist jetzt eine eigene, klickbare Kachel im selben Look wie die Bestellstatus-Kacheln oben (Wunsch: "sollen aussehen wie screen2"). Klick auf eine Kachel öffnet direkt darunter ein Panel mit der vollständigen Liste (nicht mehr nur den Top 4/8) — nochmal klicken klappt es wieder ein. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5df0643a9d
commit
03a9fa234c
@@ -158,7 +158,10 @@ const lang: Locale = "de";
|
||||
<p>Noch keine ausgewerteten Verkäufe.</p>
|
||||
<p class="small">Hier siehst du bald, welche Produkte je Kategorie am besten laufen (stornierte Bestellungen zählen nicht mit).</p>
|
||||
</div>
|
||||
<div class="verwaltung-auswertung-grid" id="verwaltung-kat-grid"></div>
|
||||
{/* Jede Kategorie ist jetzt eine eigene Kachel im selben Stil wie die
|
||||
Bestellstatus-Kacheln oben — Klick öffnet direkt darunter die volle Produktliste. */}
|
||||
<div class="verwaltung-analyse-grid" id="verwaltung-kat-grid"></div>
|
||||
<div class="verwaltung-analyse-detail" id="verwaltung-kat-detail" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -173,7 +176,8 @@ const lang: Locale = "de";
|
||||
<p>Noch keine Bestellungen.</p>
|
||||
<p class="small">Sobald Bestellungen eingehen, siehst du hier den Anteil je Land (DE/AT/CH/LU).</p>
|
||||
</div>
|
||||
<div class="verwaltung-bar-list" id="verwaltung-land-list"></div>
|
||||
<div class="verwaltung-analyse-grid" id="verwaltung-land-grid"></div>
|
||||
<div class="verwaltung-analyse-detail" id="verwaltung-land-detail" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -188,7 +192,8 @@ const lang: Locale = "de";
|
||||
<p>Noch keine wiederkehrende Kundschaft.</p>
|
||||
<p class="small">Sobald jemand zum zweiten Mal bestellt, erscheint sie hier — sortiert nach bisherigem Umsatz.</p>
|
||||
</div>
|
||||
<div class="verwaltung-bar-list" id="verwaltung-kunden-list"></div>
|
||||
<div class="verwaltung-analyse-grid" id="verwaltung-kunden-grid"></div>
|
||||
<div class="verwaltung-analyse-detail" id="verwaltung-kunden-detail" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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) => `
|
||||
<div class="card verwaltung-kat-card">
|
||||
<h3>${k.icon} ${escapeHtml(k.name)}</h3>
|
||||
<div class="verwaltung-bar-list">
|
||||
${k.produkte.map((p) => `
|
||||
<div class="verwaltung-bar-row">
|
||||
<div class="verwaltung-bar-head"><span>${escapeHtml(p.name)}</span><span class="small">${p.menge}× verkauft</span></div>
|
||||
<div class="verwaltung-bar-track"><div class="verwaltung-bar-fill" style="width:${p.prozent}%"></div></div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="card verwaltung-stat-tile" data-kat-tile="${escapeHtml(k.slug)}">
|
||||
<span class="verwaltung-stat-icon" aria-hidden="true">${k.icon}</span>
|
||||
<span class="verwaltung-stat-value">${k.gesamtMenge}</span>
|
||||
<span class="verwaltung-stat-label">${escapeHtml(k.name)}</span>
|
||||
</button>
|
||||
`).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) => `
|
||||
<div class="verwaltung-bar-row">
|
||||
<div class="verwaltung-bar-head"><span>${l.flagge} ${escapeHtml(l.name)}</span><span class="small">${l.count} Bestellungen · ${l.prozent}%</span></div>
|
||||
<div class="verwaltung-bar-track"><div class="verwaltung-bar-fill verwaltung-bar-fill-land" style="width:${l.balken}%"></div></div>
|
||||
</div>
|
||||
`).join("");
|
||||
landGrid.innerHTML = auswertungLaender.map((l) => `
|
||||
<button type="button" class="card verwaltung-stat-tile" data-land-tile="${l.land}">
|
||||
<span class="verwaltung-stat-icon" aria-hidden="true">${l.flagge}</span>
|
||||
<span class="verwaltung-stat-value">${l.count}</span>
|
||||
<span class="verwaltung-stat-label">${escapeHtml(l.name)} · ${l.prozent}%</span>
|
||||
</button>
|
||||
`).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) => `
|
||||
<div class="verwaltung-bar-row">
|
||||
<div class="verwaltung-bar-head"><span>👑 ${escapeHtml(k.name)} <span class="small">(${escapeHtml(k.email)})</span></span><span class="small">${formatPrice(k.summe, lang)} · ${k.anzahl}× bestellt</span></div>
|
||||
<div class="verwaltung-bar-track"><div class="verwaltung-bar-fill verwaltung-bar-fill-kunden" style="width:${Math.round((k.summe / maxKundenUmsatz) * 100)}%"></div></div>
|
||||
</div>
|
||||
kundenGrid.innerHTML = besteKunden.map((k) => `
|
||||
<button type="button" class="card verwaltung-stat-tile" data-kunden-tile="${escapeHtml(k.email)}">
|
||||
<span class="verwaltung-stat-icon" aria-hidden="true">👑</span>
|
||||
<span class="verwaltung-stat-value">${formatPrice(k.summe, lang)}</span>
|
||||
<span class="verwaltung-stat-label">${escapeHtml(k.name)} · ${k.anzahl}×</span>
|
||||
</button>
|
||||
`).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 = `
|
||||
<div class="verwaltung-analyse-detail-head">
|
||||
<h3>${k.icon} ${escapeHtml(k.name)} — ${k.produkte.length} Produkt${k.produkte.length === 1 ? "" : "e"}</h3>
|
||||
<button type="button" class="verwaltung-analyse-close" data-close-detail="kat" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
<div class="verwaltung-bar-list">
|
||||
${k.produkte.map((p) => `
|
||||
<div class="verwaltung-bar-row">
|
||||
<div class="verwaltung-bar-head"><span>${escapeHtml(p.name)}</span><span class="small">${p.menge}× verkauft</span></div>
|
||||
<div class="verwaltung-bar-track"><div class="verwaltung-bar-fill" style="width:${p.prozent}%"></div></div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
<div class="verwaltung-analyse-detail-head">
|
||||
<h3>${l.flagge} ${escapeHtml(l.name)} — ${bestellungen.length} Bestellung${bestellungen.length === 1 ? "" : "en"}</h3>
|
||||
<button type="button" class="verwaltung-analyse-close" data-close-detail="land" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
${bestellungen.map((b) => `
|
||||
<div class="verwaltung-analyse-order-row">
|
||||
<span>${escapeHtml(b.bestellnummer || `#${b.id}`)} · ${escapeHtml(b.kunde_name)}</span>
|
||||
<span class="small">${new Date(b.created_at).toLocaleDateString("de-DE")} · ${formatPrice(Number(b.summe), lang)}</span>
|
||||
</div>
|
||||
`).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 = `
|
||||
<div class="verwaltung-analyse-detail-head">
|
||||
<h3>👑 ${escapeHtml(k.name)} <span class="small">(${escapeHtml(email)})</span> — ${bestellungen.length} Bestellung${bestellungen.length === 1 ? "" : "en"}</h3>
|
||||
<button type="button" class="verwaltung-analyse-close" data-close-detail="kunden" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
${bestellungen.map((b) => `
|
||||
<div class="verwaltung-analyse-order-row">
|
||||
<span>${escapeHtml(b.bestellnummer || `#${b.id}`)}</span>
|
||||
<span class="small">${new Date(b.created_at).toLocaleDateString("de-DE")} · ${formatPrice(Number(b.summe), lang)}</span>
|
||||
</div>
|
||||
`).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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user