Shop-Filter: echte, automatisch berechnete Bestseller (Top 6) anzeigbar
Neues Kästchen "🔥 Nur Bestseller (Top 6)" im Shop-Filter (alle 4 Sprachen) — zeigt beim Anhaken
genau die 6 Produkte mit den meisten echten Verkäufen. Keine manuelle Pflege nötig: neuer
öffentlicher Endpunkt GET /api/bestseller berechnet die Rangliste live per SQL-Aggregation aus
den echten Bestelldaten (order_items JOIN orders, GROUP BY Produkt-Slug, SUM der Menge), NICHT
die manuell im Admin-Bereich setzbare "Bestseller"-Auszeichnung.
Zählweise: stornierte und noch nicht bezahlte (zahlungOffen) Bestellungen zählen nicht mit (sonst
würden Fehlbestellungen/nie eingegangene Zahlungen die Rangliste verfälschen), ebenso wenig
kostenlose Positionen wie der Abo-Teddy (kein echtes Kaufinteresse). Liefert ausschließlich
Produkt-Slugs zurück, keinerlei Bestell-/Kundendaten — bleiben weiterhin nur in /api/verwaltung/*.
Verifiziert: SQL-Aggregation mit Testdaten geprüft (korrekte Rangfolge, korrekter Ausschluss von
storniert/zahlungOffen/gratis, LIMIT 6 exakt). Frontend-Verdrahtung (Checkbox -> fetch -> Filter)
per echtem Browsertest mit vorgetäuschter API-Antwort bestätigt (checken zeigt genau die
vorgegebenen Produkte, abwählen zeigt wieder alle).
This commit is contained in:
@@ -60,6 +60,14 @@ const t = useTranslations(lang);
|
||||
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
|
||||
<input id="f-verfuegbar" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
{/* Echte, automatisch aus den Bestelldaten berechnete Bestseller (siehe
|
||||
functions/api/bestseller.js) — NICHT die manuell im Admin-Bereich setzbare
|
||||
"🔥 Bestseller"-Auszeichnung. Zeigt beim Anhaken immer genau die 6 Produkte mit den
|
||||
meisten echten Verkäufen, automatisch aktuell bei jeder neuen Bestellung. */}
|
||||
<label for="f-bestseller">{t.shop.onlyBestseller}</label>
|
||||
<input id="f-bestseller" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span id="f-bewertung-label">{t.shop.minRatingLabel}</span>
|
||||
<div class="cat-filter" id="rating-filter" data-dropdown>
|
||||
@@ -133,6 +141,7 @@ const t = useTranslations(lang);
|
||||
const preisRange = document.getElementById("f-preis");
|
||||
const preisValue = document.getElementById("f-preis-value");
|
||||
const verfuegbarChk = document.getElementById("f-verfuegbar");
|
||||
const bestsellerChk = document.getElementById("f-bestseller");
|
||||
const bewertungSel = document.getElementById("f-bewertung");
|
||||
const sortSel = document.getElementById("sort");
|
||||
const resultCount = document.getElementById("result-count");
|
||||
@@ -143,10 +152,16 @@ const t = useTranslations(lang);
|
||||
return unitFirst ? `${currencySymbol}${v}` : `${v} ${currencySymbol}`;
|
||||
}
|
||||
|
||||
// Echte, automatisch aus den Bestelldaten berechnete Top-6-Bestseller (siehe
|
||||
// functions/api/bestseller.js) — leer, bis die Antwort da ist. Wird einmal beim Laden der
|
||||
// Seite abgerufen (siehe ladeBestseller() weiter unten), nicht bei jedem Checkbox-Klick neu.
|
||||
let bestsellerSlugs = [];
|
||||
|
||||
function apply() {
|
||||
const kat = kategorieSel.value;
|
||||
const maxPreis = Number(preisRange.value);
|
||||
const nurVerfuegbar = verfuegbarChk.checked;
|
||||
const nurBestseller = bestsellerChk.checked;
|
||||
const minBewertung = Number(bewertungSel.value);
|
||||
preisValue.textContent = formatPrice(maxPreis);
|
||||
|
||||
@@ -159,8 +174,9 @@ const t = useTranslations(lang);
|
||||
: slot.dataset.kategorie === kat;
|
||||
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
|
||||
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
|
||||
const matchBestseller = !nurBestseller || bestsellerSlugs.includes(slot.dataset.slug);
|
||||
const matchBewertung = Number(slot.dataset.bewertung) >= minBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBestseller && matchBewertung;
|
||||
slot.style.display = show ? "" : "none";
|
||||
if (show) visible++;
|
||||
});
|
||||
@@ -180,7 +196,23 @@ const t = useTranslations(lang);
|
||||
sorted.forEach((el) => grid.appendChild(el));
|
||||
}
|
||||
|
||||
[preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
[preisRange, verfuegbarChk, bestsellerChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
|
||||
// Lädt einmal beim Seitenaufruf die echten Top-6-Bestseller (siehe functions/api/bestseller.js,
|
||||
// berechnet live aus den tatsächlichen Bestellungen — keine manuelle Pflege nötig). Schlägt der
|
||||
// Abruf fehl (z.B. offline), bleibt die Liste einfach leer, statt die ganze Seite zu stören —
|
||||
// das Kästchen zeigt dann schlicht "keine Produkte gefunden", bis es wieder klappt.
|
||||
async function ladeBestseller() {
|
||||
try {
|
||||
const res = await fetch("/api/bestseller");
|
||||
const data = await res.json().catch(() => null);
|
||||
bestsellerSlugs = data?.ok && Array.isArray(data.slugs) ? data.slugs : [];
|
||||
} catch {
|
||||
bestsellerSlugs = [];
|
||||
}
|
||||
if (bestsellerChk.checked) apply();
|
||||
}
|
||||
ladeBestseller();
|
||||
|
||||
// Eigene Dropdowns (statt nativer <select>-Felder): Der Browser kann in seiner eigenen
|
||||
// Auswahlliste weder unsere Kategorie-Struktur noch Verlaufs-Farben darstellen — deshalb hier
|
||||
|
||||
@@ -60,6 +60,14 @@ const t = useTranslations(lang);
|
||||
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
|
||||
<input id="f-verfuegbar" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
{/* Echte, automatisch aus den Bestelldaten berechnete Bestseller (siehe
|
||||
functions/api/bestseller.js) — NICHT die manuell im Admin-Bereich setzbare
|
||||
"🔥 Bestseller"-Auszeichnung. Zeigt beim Anhaken immer genau die 6 Produkte mit den
|
||||
meisten echten Verkäufen, automatisch aktuell bei jeder neuen Bestellung. */}
|
||||
<label for="f-bestseller">{t.shop.onlyBestseller}</label>
|
||||
<input id="f-bestseller" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span id="f-bewertung-label">{t.shop.minRatingLabel}</span>
|
||||
<div class="cat-filter" id="rating-filter" data-dropdown>
|
||||
@@ -133,6 +141,7 @@ const t = useTranslations(lang);
|
||||
const preisRange = document.getElementById("f-preis");
|
||||
const preisValue = document.getElementById("f-preis-value");
|
||||
const verfuegbarChk = document.getElementById("f-verfuegbar");
|
||||
const bestsellerChk = document.getElementById("f-bestseller");
|
||||
const bewertungSel = document.getElementById("f-bewertung");
|
||||
const sortSel = document.getElementById("sort");
|
||||
const resultCount = document.getElementById("result-count");
|
||||
@@ -143,10 +152,16 @@ const t = useTranslations(lang);
|
||||
return unitFirst ? `${currencySymbol}${v}` : `${v} ${currencySymbol}`;
|
||||
}
|
||||
|
||||
// Echte, automatisch aus den Bestelldaten berechnete Top-6-Bestseller (siehe
|
||||
// functions/api/bestseller.js) — leer, bis die Antwort da ist. Wird einmal beim Laden der
|
||||
// Seite abgerufen (siehe ladeBestseller() weiter unten), nicht bei jedem Checkbox-Klick neu.
|
||||
let bestsellerSlugs = [];
|
||||
|
||||
function apply() {
|
||||
const kat = kategorieSel.value;
|
||||
const maxPreis = Number(preisRange.value);
|
||||
const nurVerfuegbar = verfuegbarChk.checked;
|
||||
const nurBestseller = bestsellerChk.checked;
|
||||
const minBewertung = Number(bewertungSel.value);
|
||||
preisValue.textContent = formatPrice(maxPreis);
|
||||
|
||||
@@ -159,8 +174,9 @@ const t = useTranslations(lang);
|
||||
: slot.dataset.kategorie === kat;
|
||||
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
|
||||
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
|
||||
const matchBestseller = !nurBestseller || bestsellerSlugs.includes(slot.dataset.slug);
|
||||
const matchBewertung = Number(slot.dataset.bewertung) >= minBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBestseller && matchBewertung;
|
||||
slot.style.display = show ? "" : "none";
|
||||
if (show) visible++;
|
||||
});
|
||||
@@ -180,7 +196,23 @@ const t = useTranslations(lang);
|
||||
sorted.forEach((el) => grid.appendChild(el));
|
||||
}
|
||||
|
||||
[preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
[preisRange, verfuegbarChk, bestsellerChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
|
||||
// Lädt einmal beim Seitenaufruf die echten Top-6-Bestseller (siehe functions/api/bestseller.js,
|
||||
// berechnet live aus den tatsächlichen Bestellungen — keine manuelle Pflege nötig). Schlägt der
|
||||
// Abruf fehl (z.B. offline), bleibt die Liste einfach leer, statt die ganze Seite zu stören —
|
||||
// das Kästchen zeigt dann schlicht "keine Produkte gefunden", bis es wieder klappt.
|
||||
async function ladeBestseller() {
|
||||
try {
|
||||
const res = await fetch("/api/bestseller");
|
||||
const data = await res.json().catch(() => null);
|
||||
bestsellerSlugs = data?.ok && Array.isArray(data.slugs) ? data.slugs : [];
|
||||
} catch {
|
||||
bestsellerSlugs = [];
|
||||
}
|
||||
if (bestsellerChk.checked) apply();
|
||||
}
|
||||
ladeBestseller();
|
||||
|
||||
// Eigene Dropdowns (statt nativer <select>-Felder): Der Browser kann in seiner eigenen
|
||||
// Auswahlliste weder unsere Kategorie-Struktur noch Verlaufs-Farben darstellen — deshalb hier
|
||||
|
||||
@@ -60,6 +60,14 @@ const t = useTranslations(lang);
|
||||
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
|
||||
<input id="f-verfuegbar" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
{/* Echte, automatisch aus den Bestelldaten berechnete Bestseller (siehe
|
||||
functions/api/bestseller.js) — NICHT die manuell im Admin-Bereich setzbare
|
||||
"🔥 Bestseller"-Auszeichnung. Zeigt beim Anhaken immer genau die 6 Produkte mit den
|
||||
meisten echten Verkäufen, automatisch aktuell bei jeder neuen Bestellung. */}
|
||||
<label for="f-bestseller">{t.shop.onlyBestseller}</label>
|
||||
<input id="f-bestseller" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span id="f-bewertung-label">{t.shop.minRatingLabel}</span>
|
||||
<div class="cat-filter" id="rating-filter" data-dropdown>
|
||||
@@ -133,6 +141,7 @@ const t = useTranslations(lang);
|
||||
const preisRange = document.getElementById("f-preis");
|
||||
const preisValue = document.getElementById("f-preis-value");
|
||||
const verfuegbarChk = document.getElementById("f-verfuegbar");
|
||||
const bestsellerChk = document.getElementById("f-bestseller");
|
||||
const bewertungSel = document.getElementById("f-bewertung");
|
||||
const sortSel = document.getElementById("sort");
|
||||
const resultCount = document.getElementById("result-count");
|
||||
@@ -143,10 +152,16 @@ const t = useTranslations(lang);
|
||||
return unitFirst ? `${currencySymbol}${v}` : `${v} ${currencySymbol}`;
|
||||
}
|
||||
|
||||
// Echte, automatisch aus den Bestelldaten berechnete Top-6-Bestseller (siehe
|
||||
// functions/api/bestseller.js) — leer, bis die Antwort da ist. Wird einmal beim Laden der
|
||||
// Seite abgerufen (siehe ladeBestseller() weiter unten), nicht bei jedem Checkbox-Klick neu.
|
||||
let bestsellerSlugs = [];
|
||||
|
||||
function apply() {
|
||||
const kat = kategorieSel.value;
|
||||
const maxPreis = Number(preisRange.value);
|
||||
const nurVerfuegbar = verfuegbarChk.checked;
|
||||
const nurBestseller = bestsellerChk.checked;
|
||||
const minBewertung = Number(bewertungSel.value);
|
||||
preisValue.textContent = formatPrice(maxPreis);
|
||||
|
||||
@@ -159,8 +174,9 @@ const t = useTranslations(lang);
|
||||
: slot.dataset.kategorie === kat;
|
||||
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
|
||||
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
|
||||
const matchBestseller = !nurBestseller || bestsellerSlugs.includes(slot.dataset.slug);
|
||||
const matchBewertung = Number(slot.dataset.bewertung) >= minBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBestseller && matchBewertung;
|
||||
slot.style.display = show ? "" : "none";
|
||||
if (show) visible++;
|
||||
});
|
||||
@@ -180,7 +196,23 @@ const t = useTranslations(lang);
|
||||
sorted.forEach((el) => grid.appendChild(el));
|
||||
}
|
||||
|
||||
[preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
[preisRange, verfuegbarChk, bestsellerChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
|
||||
// Lädt einmal beim Seitenaufruf die echten Top-6-Bestseller (siehe functions/api/bestseller.js,
|
||||
// berechnet live aus den tatsächlichen Bestellungen — keine manuelle Pflege nötig). Schlägt der
|
||||
// Abruf fehl (z.B. offline), bleibt die Liste einfach leer, statt die ganze Seite zu stören —
|
||||
// das Kästchen zeigt dann schlicht "keine Produkte gefunden", bis es wieder klappt.
|
||||
async function ladeBestseller() {
|
||||
try {
|
||||
const res = await fetch("/api/bestseller");
|
||||
const data = await res.json().catch(() => null);
|
||||
bestsellerSlugs = data?.ok && Array.isArray(data.slugs) ? data.slugs : [];
|
||||
} catch {
|
||||
bestsellerSlugs = [];
|
||||
}
|
||||
if (bestsellerChk.checked) apply();
|
||||
}
|
||||
ladeBestseller();
|
||||
|
||||
// Eigene Dropdowns (statt nativer <select>-Felder): Der Browser kann in seiner eigenen
|
||||
// Auswahlliste weder unsere Kategorie-Struktur noch Verlaufs-Farben darstellen — deshalb hier
|
||||
|
||||
@@ -65,6 +65,14 @@ const t = useTranslations(lang);
|
||||
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
|
||||
<input id="f-verfuegbar" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
{/* Echte, automatisch aus den Bestelldaten berechnete Bestseller (siehe
|
||||
functions/api/bestseller.js) — NICHT die manuell im Admin-Bereich setzbare
|
||||
"🔥 Bestseller"-Auszeichnung. Zeigt beim Anhaken immer genau die 6 Produkte mit den
|
||||
meisten echten Verkäufen, automatisch aktuell bei jeder neuen Bestellung. */}
|
||||
<label for="f-bestseller">{t.shop.onlyBestseller}</label>
|
||||
<input id="f-bestseller" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span id="f-bewertung-label">{t.shop.minRatingLabel}</span>
|
||||
{/* Eigenes Dropdown statt eines nativen <select>: In der Browser-eigenen Auswahlliste
|
||||
@@ -142,6 +150,7 @@ const t = useTranslations(lang);
|
||||
const preisRange = document.getElementById("f-preis");
|
||||
const preisValue = document.getElementById("f-preis-value");
|
||||
const verfuegbarChk = document.getElementById("f-verfuegbar");
|
||||
const bestsellerChk = document.getElementById("f-bestseller");
|
||||
const bewertungSel = document.getElementById("f-bewertung");
|
||||
const sortSel = document.getElementById("sort");
|
||||
const resultCount = document.getElementById("result-count");
|
||||
@@ -152,10 +161,16 @@ const t = useTranslations(lang);
|
||||
return unitFirst ? `${currencySymbol}${v}` : `${v} ${currencySymbol}`;
|
||||
}
|
||||
|
||||
// Echte, automatisch aus den Bestelldaten berechnete Top-6-Bestseller (siehe
|
||||
// functions/api/bestseller.js) — leer, bis die Antwort da ist. Wird einmal beim Laden der
|
||||
// Seite abgerufen (siehe ladeBestseller() weiter unten), nicht bei jedem Checkbox-Klick neu.
|
||||
let bestsellerSlugs = [];
|
||||
|
||||
function apply() {
|
||||
const kat = kategorieSel.value;
|
||||
const maxPreis = Number(preisRange.value);
|
||||
const nurVerfuegbar = verfuegbarChk.checked;
|
||||
const nurBestseller = bestsellerChk.checked;
|
||||
const minBewertung = Number(bewertungSel.value);
|
||||
preisValue.textContent = formatPrice(maxPreis);
|
||||
|
||||
@@ -168,8 +183,9 @@ const t = useTranslations(lang);
|
||||
: slot.dataset.kategorie === kat;
|
||||
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
|
||||
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
|
||||
const matchBestseller = !nurBestseller || bestsellerSlugs.includes(slot.dataset.slug);
|
||||
const matchBewertung = Number(slot.dataset.bewertung) >= minBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBestseller && matchBewertung;
|
||||
slot.style.display = show ? "" : "none";
|
||||
if (show) visible++;
|
||||
});
|
||||
@@ -189,7 +205,23 @@ const t = useTranslations(lang);
|
||||
sorted.forEach((el) => grid.appendChild(el));
|
||||
}
|
||||
|
||||
[preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
[preisRange, verfuegbarChk, bestsellerChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
|
||||
// Lädt einmal beim Seitenaufruf die echten Top-6-Bestseller (siehe functions/api/bestseller.js,
|
||||
// berechnet live aus den tatsächlichen Bestellungen — keine manuelle Pflege nötig). Schlägt der
|
||||
// Abruf fehl (z.B. offline), bleibt die Liste einfach leer, statt die ganze Seite zu stören —
|
||||
// das Kästchen zeigt dann schlicht "keine Produkte gefunden", bis es wieder klappt.
|
||||
async function ladeBestseller() {
|
||||
try {
|
||||
const res = await fetch("/api/bestseller");
|
||||
const data = await res.json().catch(() => null);
|
||||
bestsellerSlugs = data?.ok && Array.isArray(data.slugs) ? data.slugs : [];
|
||||
} catch {
|
||||
bestsellerSlugs = [];
|
||||
}
|
||||
if (bestsellerChk.checked) apply();
|
||||
}
|
||||
ladeBestseller();
|
||||
|
||||
// Eigene Dropdowns (statt nativer <select>-Felder): Der Browser kann in seiner eigenen
|
||||
// Auswahlliste weder unsere Kategorie-Struktur noch Verlaufs-Farben darstellen — deshalb hier
|
||||
|
||||
Reference in New Issue
Block a user