Echtes Bestellsystem: Cloudflare D1 statt Beispieldaten in der Verwaltung

Ersetzt die Phase-1-Beispieldaten in /verwaltung/ durch ein echtes, funktionierendes
Bestellsystem:

- Neue D1-Datenbank (vans-diy-bastelbedarf-db) mit orders/order_items-Tabellen,
  an das Pages-Projekt gebunden (wrangler.toml).
- Neue API-Routen (Pages Functions): POST /api/orders legt beim Checkout eine echte
  Bestellung an (mit Validierung); GET/PATCH /api/verwaltung/orders(/:id) listen bzw.
  ändern Bestellungen — beide zusätzlich zur Middleware nochmal eigenständig gegen die
  Zugangscode-Sitzung geprüft (Verteidigung in der Tiefe). Gemeinsame Auth-Logik aus
  _middleware.js nach functions/_shared/auth.js ausgelagert.
- Checkout (4 Sprachen) sendet beim Absenden die echte Bestellung an die API, zeigt bei
  einem Fehler eine klare Meldung statt einfach auf die Danke-Seite weiterzuleiten.
- /verwaltung/ lädt Bestellungen jetzt live per fetch(), mit Lade-/Fehlerzustand, und
  erlaubt direktes Ändern des Bestellstatus per Dropdown (sofort in der Datenbank
  gespeichert). Neuer Status "storniert" (zählt nicht in Umsatz/Auswertung mit).

Vollständig lokal end-to-end getestet (wrangler pages dev + lokale D1): echter
Browser-Checkout -> Bestellung landet in der Datenbank -> erscheint sofort in der
Verwaltung -> Status ändern bleibt nach Neuladen bestehen.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-03 20:32:46 +02:00
co-authored by Claude Sonnet 5
parent 9170b9bb22
commit fb7a7b76c2
14 changed files with 1030 additions and 333 deletions
+66 -10
View File
@@ -180,7 +180,7 @@ const t = useTranslations(lang);
<script type="module">
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
import { products } from "../../../data/products";
import { products, getProduct } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account";
@@ -194,6 +194,8 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const cart = getCart().filter(istAusgewaehlt);
const subtotal = cartTotal();
let currentTotal = 0;
let currentShipping = 0;
let currentRabattGesamt = 0;
function renderSummary() {
const land = landSelect.value;
@@ -206,6 +208,8 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
const aboDiscountBetrag = aboDiscount(subtotal);
const total = Math.max(0, subtotal - totalDiscount(subtotal)) + shipping;
currentTotal = total;
currentShipping = shipping;
currentRabattGesamt = totalDiscount(subtotal);
summary.innerHTML = `
${cart.map((i) => {
@@ -331,20 +335,72 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
renderSummary();
});
document.getElementById("checkout-form").addEventListener("submit", (e) => {
const orderForm = document.getElementById("checkout-form");
const submitBtn = orderForm.querySelector('button[type="submit"]');
const orderErrorEl = document.createElement("p");
orderErrorEl.className = "small";
orderErrorEl.style.color = "var(--c-sale)";
orderErrorEl.style.display = "none";
submitBtn.insertAdjacentElement("beforebegin", orderErrorEl);
orderForm.addEventListener("submit", async (e) => {
e.preventDefault();
if (cart.length === 0) {
alert(emptyCartAlert);
return;
}
// Treuebonus (siehe scripts/account.ts): Eine Bestellung zählt entweder für den NÄCHSTEN
// Durchlauf (registriereAbgeschlosseneBestellung) ODER löst eine bereits wartende Belohnung
// ein (treuebonusEinloesen) — nie beides gleichzeitig.
if (treuebonusDiscount() > 0) {
treuebonusEinloesen();
} else {
registriereAbgeschlosseneBestellung();
orderErrorEl.style.display = "none";
submitBtn.disabled = true;
const urspruenglicherBtnText = submitBtn.textContent;
submitBtn.textContent = "…";
const zahlungsart = document.querySelector('input[name="pay"]:checked')?.value || "";
const payload = {
land: landSelect.value,
kunde: {
name: `${document.getElementById("vorname").value.trim()} ${document.getElementById("nachname").value.trim()}`.trim(),
email: document.getElementById("email").value.trim(),
strasse: document.getElementById("strasse").value.trim(),
plz: document.getElementById("plz").value.trim(),
ort: document.getElementById("ort").value.trim(),
},
zahlungsart,
artikel: cart.map((i) => {
const produkt = getProduct(i.slug);
const einzelpreis = i.gratis ? 0 : zeilenpreisFuer(i) / i.menge;
return { slug: i.slug, name: i.name, kategorie: produkt?.kategorie ?? "", menge: i.menge, preis: einzelpreis, gratis: !!i.gratis };
}),
zwischensumme: subtotal,
versandkosten: currentShipping,
rabattGesamt: currentRabattGesamt,
summe: currentTotal,
treuebonusVerwendet: treuebonusDiscount() > 0,
aboRabattVerwendet: aboDiscount(subtotal) > 0,
};
try {
const res = await fetch("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) {
throw new Error(data?.error || "Bestellung konnte nicht gespeichert werden.");
}
if (treuebonusDiscount() > 0) {
treuebonusEinloesen();
} else {
registriereAbgeschlosseneBestellung();
}
location.href = thankYouPath;
} catch (err) {
orderErrorEl.textContent = "⚠️ " + (err instanceof Error ? err.message : "Bestellung konnte nicht gespeichert werden. Bitte nochmal versuchen.");
orderErrorEl.style.display = "block";
submitBtn.disabled = false;
submitBtn.textContent = urspruenglicherBtnText;
}
location.href = thankYouPath;
});
</script>