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
@@ -183,7 +183,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";
|
||||
@@ -197,6 +197,10 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
const cart = getCart().filter(istAusgewaehlt);
|
||||
const subtotal = cartTotal();
|
||||
let currentTotal = 0;
|
||||
// Für den echten Bestell-API-Aufruf beim Absenden gebraucht (siehe Submit-Handler unten) —
|
||||
// renderSummary() aktualisiert diese bei jeder Land-/Gutschein-Änderung mit.
|
||||
let currentShipping = 0;
|
||||
let currentRabattGesamt = 0;
|
||||
|
||||
function renderSummary() {
|
||||
const land = landSelect.value;
|
||||
@@ -209,6 +213,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) => {
|
||||
@@ -334,22 +340,82 @@ 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. Nur eine erfolgreich abgeschickte,
|
||||
// vollständig bezahlte Bestellung zählt hier; abgebrochene/nicht abgeschickte Bestellungen
|
||||
// rufen diese Funktionen nie auf.
|
||||
if (treuebonusDiscount() > 0) {
|
||||
treuebonusEinloesen();
|
||||
} else {
|
||||
registriereAbgeschlosseneBestellung();
|
||||
|
||||
// Echte Bestellung in der Datenbank anlegen (siehe functions/api/orders.js) — Phase 1 hat
|
||||
// kein echtes Zahlungs-Backend, jede vollständig abgeschickte Bestellung gilt sofort als
|
||||
// "bezahlt" (Server setzt diesen Status automatisch). Erst NACH erfolgreicher Antwort geht
|
||||
// es weiter — bei einem Fehler bleibt man im Checkout und sieht eine klare Meldung, statt
|
||||
// trotzdem auf die Danke-Seite zu landen, obwohl die Bestellung nirgends gespeichert wurde.
|
||||
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.");
|
||||
}
|
||||
|
||||
// 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. Läuft ERST NACH erfolgreichem
|
||||
// Speichern der echten Bestellung, damit lokaler Kontostand und echte Bestelldaten nie
|
||||
// auseinanderlaufen.
|
||||
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