Files
qcigano 2efba64d4f 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).
2026-08-04 16:05:57 +02:00

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: [] });
}
}