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).
52 lines
2.8 KiB
JavaScript
52 lines
2.8 KiB
JavaScript
/* =====================================================================
|
|
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: [] });
|
|
}
|
|
}
|