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:
@@ -0,0 +1,51 @@
|
||||
/* =====================================================================
|
||||
GET /api/bestseller — liefert die Slugs der 6 echten meistverkauften Produkte, automatisch aus
|
||||
den echten Bestelldaten berechnet (04.08.2026, auf ausdrücklichen Wunsch: "die Seite soll von
|
||||
alleine wissen, welche Sachen am meisten verkauft werden").
|
||||
|
||||
BEWUSST öffentlich (kein Zugangscode-Sonderfall in functions/_middleware.js nötig): Diese Route
|
||||
verhält sich genau wie /api/orders (Bestellung anlegen) — während der Vorschau-Phase greift die
|
||||
normale Zugangsschranke wie bei jeder anderen /api/*-Route auch (nur qciga/VanVan kommen ran),
|
||||
sobald der Shop später per SITE_PUBLIC=true live geht, wird sie automatisch mit-öffentlich,
|
||||
ohne dass hier etwas geändert werden muss. Liefert NUR eine Liste von Produkt-Slugs zurück —
|
||||
keinerlei Bestell-/Kundendaten (Namen, E-Mails, Adressen, Beträge), die bleiben ausschließlich
|
||||
in /api/verwaltung/* (fest hinter der doppelten Schranke).
|
||||
|
||||
Zählweise (bewusst dieselbe Grundlage wie die "Bestseller je Kategorie"-Auswertung in
|
||||
/verwaltung/, siehe src/pages/verwaltung/index.astro):
|
||||
- Storniert und noch nicht bezahlte (zahlungOffen) Bestellungen zählen NICHT mit — sonst
|
||||
würden Fehlbestellungen/nie bezahlte Überweisungen die Rangliste verfälschen.
|
||||
- Kostenlose Positionen (gratis=1, z.B. der Teddy aus dem Abo) zählen NICHT mit — das war kein
|
||||
echtes Kaufinteresse, sondern eine Prämie, würde sonst die Rangliste verzerren.
|
||||
- Bei Gleichstand entscheidet der Produkt-Slug alphabetisch (rein für ein stabiles, immer
|
||||
gleiches Ergebnis bei jedem Aufruf, keine fachliche Bedeutung). */
|
||||
|
||||
import { json } from "../_shared/http.js";
|
||||
|
||||
export async function onRequestGet(context) {
|
||||
const { env } = context;
|
||||
|
||||
// Kein DB-Binding (z.B. sehr früher lokaler Dev-Stand ohne D1) oder noch gar keine Bestellungen
|
||||
// -> einfach eine leere Liste, kein Fehler. Das Bestseller-Kästchen im Shop-Filter zeigt dann
|
||||
// schlicht "keine Produkte gefunden", genau wie bei jeder anderen Filterkombination ohne Treffer.
|
||||
if (!env.DB) {
|
||||
return json(200, { ok: true, slugs: [] });
|
||||
}
|
||||
|
||||
try {
|
||||
const { results } = await env.DB.prepare(
|
||||
`SELECT oi.produkt_slug AS slug, SUM(oi.menge) AS verkauft
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE o.status NOT IN ('storniert', 'zahlungOffen') AND oi.gratis = 0
|
||||
GROUP BY oi.produkt_slug
|
||||
ORDER BY verkauft DESC, slug ASC
|
||||
LIMIT 6`
|
||||
).all();
|
||||
return json(200, { ok: true, slugs: results.map((r) => r.slug) });
|
||||
} catch {
|
||||
// Lieber still eine leere Liste als einen Fehler an den Shop-Filter durchreichen — eine
|
||||
// kaputte Bestseller-Berechnung soll nie den ganzen Filterbereich lahmlegen.
|
||||
return json(200, { ok: true, slugs: [] });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user