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:
co-authored by
Claude Sonnet 5
parent
9170b9bb22
commit
fb7a7b76c2
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user