Files
vans-diy-bastelbedarf/src/pages/fr/checkout/index.astro
T
qciganoandClaude Sonnet 5 fb7a7b76c2 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]>
2026-08-03 20:32:46 +02:00

407 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="Paiement" description="Finaliser la commande chez Van's DIY & Bastelbedarf." lang={lang} path="/checkout/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.checkout.eyebrow}</span>
<h1>{t.checkout.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container checkout-layout">
<form class="frm card" id="checkout-form">
<div class="checkout-split">
<div class="checkout-split-col">
<h3>{t.checkout.step2}</h3>
{/* Echte Markenlogos statt Platzhaltern: PayPal- und Klarna-Icon in den offiziellen
Markenfarben, Visa/Mastercard als unveränderte "Acceptance Marks" nebeneinander —
genau die in den jeweiligen Markenrichtlinien vorgesehene Verwendung, um eine
Zahlungsmethode im Checkout auszuweisen (siehe Recherche zu PayPal-/Klarna-/Visa-/
Mastercard-Markenrichtlinien: Logos dürfen zur Kennzeichnung akzeptierter
Zahlungsarten verwendet werden, Visa/Mastercard aber nur unverändert/unverfälscht). */}
<div class="payment-options">
<div class="pay-card" style="--brand-color:#009cde; --brand-bg:#003087;">
<label class="pay-option">
<span class="pay-icon pay-icon-brand" style="background:linear-gradient(160deg, #0091e6, #003087);">
<svg viewBox="0 0 24 24" width="30" height="30" fill="#fff" aria-hidden="true"><path d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"></path></svg>
</span>
<span>PayPal</span>
<input type="radio" name="pay" value="paypal" checked />
</label>
<div class="pay-panel-inline">
<p>{t.checkout.payPalHint}</p>
<div id="paypal-button-container"></div>
</div>
</div>
<div class="pay-card" style="--brand-color:#ffb3c7; --brand-bg:#17120f;">
<label class="pay-option">
<span class="pay-icon pay-icon-brand" style="background:linear-gradient(160deg, #ffd1de, #ffb3c7);">
<svg viewBox="0 0 24 24" width="26" height="26" fill="#0a0a0a" aria-hidden="true"><path d="M4.592 2v20H0V2h4.592zm11.46 0c0 4.194-1.583 8.105-4.415 11.068l-.278.283L17.702 22h-5.668l-6.893-9.4 1.779-1.332c2.858-2.14 4.535-5.378 4.637-8.924L11.562 2h4.49zM21.5 17a2.5 2.5 0 110 5 2.5 2.5 0 010-5z"></path></svg>
</span>
<span>Klarna</span>
<input type="radio" name="pay" value="klarna" />
</label>
<div class="pay-panel-inline">
<p>{t.checkout.klarnaHint}</p>
<button type="button" class="btn btn-outline pay-connect-btn" id="klarna-connect">{t.checkout.klarnaConnect}</button>
<p class="pay-connect-status" id="klarna-status"></p>
</div>
</div>
<div class="pay-card" style="--brand-color:#e3b23c; --brand-bg:#241f3d;">
<label class="pay-option">
<span class="pay-icon-cards" aria-hidden="true">
<span class="mini-card mini-card-visa">
<svg viewBox="0 0 24 24" width="28" height="28" fill="#1434CB" aria-hidden="true"><path d="M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-.094-.368-.175-.503-.461-.658C1.447 8.864.677 8.627 0 8.479l.046-.217h3.3a.904.904 0 01.894.764l.817 4.338 2.018-5.102zm8.033 5.049c.008-1.979-2.736-2.088-2.717-2.972.006-.269.262-.555.822-.628a3.66 3.66 0 011.913.336l.34-1.59a5.207 5.207 0 00-1.814-.333c-1.917 0-3.266 1.02-3.278 2.479-.012 1.079.963 1.68 1.698 2.04.756.367 1.01.603 1.006.931-.005.504-.602.725-1.16.734-.975.015-1.54-.263-1.992-.473l-.351 1.642c.453.208 1.289.39 2.156.398 2.037 0 3.37-1.006 3.377-2.564m5.061 2.447H24l-1.565-7.496h-1.656a.883.883 0 00-.826.55l-2.909 6.946h2.036l.405-1.12h2.488zm-2.163-2.656l1.02-2.815.588 2.815zm-8.16-4.84l-1.603 7.496H8.34l1.605-7.496z"></path></svg>
</span>
<span class="mini-card mini-card-mastercard">
<svg viewBox="0 0 32 20" width="32" height="20" aria-hidden="true"><circle cx="13" cy="10" r="7.2" fill="#EB001B"></circle><circle cx="19" cy="10" r="7.2" fill="#F79E1B"></circle><path d="M16 4.4a7.18 7.18 0 010 11.2 7.18 7.18 0 010-11.2z" fill="#FF5F00"></path></svg>
</span>
</span>
<span>{t.checkout.creditCard}<span class="pay-sub">{t.checkout.creditCardSub}</span></span>
<input type="radio" name="pay" value="kreditkarte" />
</label>
<div class="pay-panel-inline">
<p>{t.checkout.cardHint}</p>
<div class="card-fields">
<div>
<label for="cc-number">{t.checkout.cardNumber}</label>
<input type="text" id="cc-number" inputmode="numeric" placeholder="1234 5678 9012 3456" maxlength="19" />
</div>
<div class="grid grid-2">
<div><label for="cc-expiry">{t.checkout.cardExpiry}</label><input type="text" id="cc-expiry" placeholder="MM/YY" maxlength="5" /></div>
<div><label for="cc-cvc">{t.checkout.cardCvc}</label><input type="text" id="cc-cvc" inputmode="numeric" placeholder="123" maxlength="4" /></div>
</div>
</div>
</div>
</div>
<div class="pay-card" style="--brand-color:#7fa8c9; --brand-bg:#3b5a76;">
<label class="pay-option">
<span class="pay-icon" style="background:linear-gradient(160deg, #4d7599, #2c4258);">
<svg viewBox="0 0 24 24" width="27" height="27" fill="none" stroke="#fff" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 9.5 12 4l10 5.5"></path><path d="M4 9.5V19M9 9.5V19M15 9.5V19M20 9.5V19"></path><path d="M2.5 19h19"></path></svg>
</span>
<span>{t.checkout.bankTransfer}</span>
<input type="radio" name="pay" value="ueberweisung" />
</label>
<div class="pay-panel-inline">
<p>{t.checkout.bankHintReal}</p>
<div class="bank-details" id="bank-details"></div>
</div>
</div>
</div>
</div>
<div class="checkout-split-divider" aria-hidden="true"><span>🧵</span></div>
<div class="checkout-split-col">
<h3>{t.checkout.step1}</h3>
<div><label for="email">{t.checkout.email}</label><input id="email" type="email" required /></div>
<div class="grid grid-2">
<div><label for="vorname">{t.checkout.firstName}</label><input id="vorname" type="text" required /></div>
<div><label for="nachname">{t.checkout.lastName}</label><input id="nachname" type="text" required /></div>
</div>
<div><label for="strasse">{t.checkout.street}</label><input id="strasse" type="text" placeholder="1 Rue de lExemple" required /></div>
<div class="grid grid-2">
<div><label for="plz">{t.checkout.zip}</label><input id="plz" type="text" inputmode="numeric" maxlength="5" placeholder="75001" required /></div>
<div><label for="ort">{t.checkout.city}</label><input id="ort" type="text" placeholder="Paris" required /></div>
</div>
<div>
<label for="land">{t.checkout.country}</label>
<select id="land">
<option value="de">Allemagne</option>
<option value="at">Autriche</option>
<option value="ch">Suisse</option>
<option value="lu">Luxembourg</option>
</select>
</div>
<hr class="divider" />
<h3>{t.checkout.step3}</h3>
<div id="checkout-summary" class="checkout-summary"></div>
<p class="small">{t.common.vatNote}</p>
<div class="checkbox-row">
<input id="agb" type="checkbox" required />
<label for="agb">{t.checkout.agbPrefix} <a href="/agb/">{t.checkout.agbLink}</a> {t.checkout.agbSuffix}</label>
</div>
<div class="checkbox-row">
<input id="widerruf" type="checkbox" required />
<label for="widerruf">{t.checkout.revocationPrefix} <a href="/widerruf/">{t.checkout.revocationLink}</a> {t.checkout.revocationAnd} <a href="/datenschutz/">{t.checkout.privacyLink}</a> {t.checkout.revocationSuffix}</label>
</div>
</div>
</div>
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
</form>
<aside class="card checkout-aside">
<div class="coupon-row">
<label for="coupon-input">{t.cart.couponLabel}</label>
<div class="coupon-input-group">
<input type="text" id="coupon-input" placeholder={t.cart.couponPlaceholder} />
<button type="button" id="coupon-apply" class="btn btn-outline btn-sm">{t.cart.couponApply}</button>
</div>
<p class="small" id="coupon-status"></p>
</div>
<hr class="divider stitched" />
<div class="coupon-row">
<label for="partner-code-input">{t.cart.partnerCodeLabel}</label>
<div class="coupon-input-group">
<input type="text" id="partner-code-input" placeholder={t.cart.partnerCodePlaceholder} />
<button type="button" id="partner-code-apply" class="btn btn-outline btn-sm">{t.cart.partnerCodeApply}</button>
</div>
<p class="small" id="partner-code-status"></p>
</div>
<hr class="divider stitched" />
<h3>🚚 {t.checkout.shippingHintTitle}</h3>
<p class="small">{t.checkout.shippingHint}</p>
<a class="small" href="/fr/versand-zahlung/">{t.checkout.shippingLink}</a>
</aside>
</div>
</section>
<section class="section-tight">
<div class="container">
<p class="todo-note">{t.checkout.todoNote}</p>
</div>
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/fr/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, klarnaConnectLabel: t.checkout.klarnaConnect, klarnaConnectingLabel: t.checkout.klarnaConnecting, klarnaConnectedLabel: t.checkout.klarnaConnected, bankHolderLabel: t.checkout.bankHolder, bankIbanLabel: t.checkout.bankIban, bankBicLabel: t.checkout.bankBic, bankBankNameLabel: t.checkout.bankBankName, bankPlaceholderLabel: t.checkout.bankPlaceholder, partnerDiscountLabel: t.cart.partnerDiscount, partnerCodeInvalid: t.cart.partnerCodeInvalid, partnerCodeAppliedTemplate: t.cart.partnerCodeApplied("__C__"), partnerCodeRemoveLabel: t.cart.partnerCodeRemove, loyaltyDiscountLabel: t.cart.loyaltyDiscount, aboDiscountLabel: t.cart.aboDiscount, gratisPraemieLabel: t.cart.gratisPraemie }}>
// WICHTIG: define:vars-Skripte laufen als IIFE, keine "import"-Anweisungen möglich.
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel };
</script>
<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, getProduct } from "../../../data/products";
import { berechneVersandkosten } from "../../../data/versand";
import { bankverbindung, bankverbindungVollstaendig } from "../../../data/bankverbindung";
import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../../scripts/account";
const { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, klarnaConnectLabel, klarnaConnectingLabel, klarnaConnectedLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel } = window.__checkoutVars;
const summary = document.getElementById("checkout-summary");
const landSelect = document.getElementById("land");
// Nur die im Warenkorb ausgewählten Artikel werden hier bestellt — abgewählte bleiben im
// Warenkorb liegen und tauchen im Checkout gar nicht erst auf.
const cart = getCart().filter(istAusgewaehlt);
const subtotal = cartTotal();
let currentTotal = 0;
let currentShipping = 0;
let currentRabattGesamt = 0;
function renderSummary() {
const land = landSelect.value;
const shipping = cart.length === 0 ? 0 : berechneVersandkosten(land, cart.map((i) => ({ slug: i.slug, preis: i.preis, menge: i.menge })), products);
const coupon = appliedCoupon();
const discount = couponDiscount(subtotal);
const partnerCode = appliedPartnerCode();
const partnerDiscount = partnerCodeDiscount(subtotal);
const loyaltyDiscount = treuebonusDiscount();
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) => {
const zeilenpreis = zeilenpreisFuer(i);
return `<div class="row"><span>${i.menge}× ${i.name}${i.gratis ? ` <span class="small">(${gratisPraemieLabel})</span>` : ""}</span><span>${formatPrice(zeilenpreis, checkoutLang)}</span></div>`;
}).join("")}
${coupon && discount > 0 ? `<div class="row"><span>${discountLabel}</span><span>-${formatPrice(discount, checkoutLang)}</span></div>` : ""}
${partnerCode && partnerDiscount > 0 ? `<div class="row"><span>${partnerDiscountLabel}</span><span>-${formatPrice(partnerDiscount, checkoutLang)}</span></div>` : ""}
${loyaltyDiscount > 0 ? `<div class="row"><span>${loyaltyDiscountLabel}</span><span>-${formatPrice(loyaltyDiscount, checkoutLang)}</span></div>` : ""}
${aboDiscountBetrag > 0 ? `<div class="row"><span>${aboDiscountLabel}</span><span>-${formatPrice(aboDiscountBetrag, checkoutLang)}</span></div>` : ""}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(total, checkoutLang)}</span></div>
`;
const couponStatus = document.getElementById("coupon-status");
const couponInput = document.getElementById("coupon-input");
if (coupon && discount > 0) {
couponStatus.innerHTML = `✅ ${couponAppliedTemplate.replace("__C__", coupon.code)} <a href="#" id="coupon-clear">✕ ${couponRemoveLabel}</a>`;
couponInput.value = coupon.code;
const clearLink = document.getElementById("coupon-clear");
if (clearLink) clearLink.addEventListener("click", (e) => { e.preventDefault(); clearCoupon(); renderSummary(); });
} else {
couponStatus.textContent = couponInput.dataset.invalid === "1" ? couponInvalid : "";
}
const partnerStatus = document.getElementById("partner-code-status");
const partnerInput = document.getElementById("partner-code-input");
if (partnerCode && partnerDiscount > 0) {
partnerStatus.innerHTML = `✅ ${partnerCodeAppliedTemplate.replace("__C__", partnerCode.code)} <a href="#" id="partner-code-clear">✕ ${partnerCodeRemoveLabel}</a>`;
partnerInput.value = partnerCode.code;
const partnerClearLink = document.getElementById("partner-code-clear");
if (partnerClearLink) partnerClearLink.addEventListener("click", (e) => { e.preventDefault(); clearPartnerCode(); renderSummary(); });
} else {
partnerStatus.textContent = partnerInput.dataset.invalid === "1" ? partnerCodeInvalid : "";
}
}
renderSummary();
landSelect.addEventListener("change", renderSummary);
// Zahlungsart-Auswahl: der "Verbinden"-Bereich jeder Karte klappt rein über CSS
// (:has(input:checked)) sofort auf, sobald man die Karte anklickt — kein JS, keine Verzögerung.
// PayPal: echte PayPal Smart Buttons über das offizielle JS-SDK (Sandbox-Client-ID "sb" —
// kostenlos, ohne eigenes Konto testbar). Klick öffnet den echten PayPal-Login-Popup. Für den
// Live-Betrieb muss hier nur "sb" durch VanVans echte (kostenlose) PayPal-Business-Client-ID
// ersetzt werden, siehe developer.paypal.com.
let paypalLoaded = false;
function loadPayPalButtons() {
if (paypalLoaded || !document.getElementById("paypal-button-container")) return;
paypalLoaded = true;
const script = document.createElement("script");
script.src = "https://www.paypal.com/sdk/js?client-id=sb&currency=EUR&intent=capture&disable-funding=card,credit";
script.onload = () => {
if (!window.paypal) return;
window.paypal.Buttons({
style: { layout: "vertical", color: "blue", shape: "pill", label: "paypal" },
createOrder: (data, actions) => actions.order.create({
purchase_units: [{ amount: { value: Math.max(0.01, currentTotal).toFixed(2), currency_code: "EUR" } }],
}),
onApprove: (data, actions) => actions.order.capture().then(() => { location.href = thankYouPath; }),
}).render("#paypal-button-container");
};
document.head.appendChild(script);
}
loadPayPalButtons();
// Klarna: echtes "Verbinden" braucht eine serverseitige Session mit VanVans Klarna-Händlerkonto
// (Phase 2, Cloudflare Worker). Bis dahin ein sauber gestalteter Demo-Verbindungsablauf, damit
// die Oberfläche schon fertig aussieht und 1:1 austauschbar ist, sobald die Anbindung steht.
const klarnaBtn = document.getElementById("klarna-connect");
const klarnaStatus = document.getElementById("klarna-status");
function connectKlarna() {
if (!klarnaBtn || klarnaBtn.disabled) return;
klarnaBtn.disabled = true;
klarnaBtn.textContent = klarnaConnectingLabel;
setTimeout(() => {
klarnaBtn.textContent = klarnaConnectLabel;
klarnaBtn.disabled = false;
klarnaStatus.textContent = "✅ " + klarnaConnectedLabel;
}, 1100);
}
if (klarnaBtn) {
klarnaBtn.addEventListener("click", connectKlarna);
const klarnaRadio = document.querySelector('input[name="pay"][value="klarna"]');
// Verbindet automatisch, sobald die Klarna-Karte ausgewählt wird — kein separater Klick
// auf den Button nötig (der bleibt als "erneut verbinden" erhalten).
klarnaRadio?.addEventListener("change", () => { if (!klarnaStatus.textContent) connectKlarna(); });
}
// Banküberweisung (Vorkasse): braucht KEINE externe Anbindung — zeigt einfach VanVans echte,
// im Adminbereich hinterlegte Kontodaten an. Das ist die einzige der vier Zahlungsarten, die
// schon zu 100% "fertig" funktioniert.
const bankDetailsEl = document.getElementById("bank-details");
if (bankDetailsEl) {
if (bankverbindungVollstaendig(bankverbindung)) {
bankDetailsEl.innerHTML = `
<div class="bank-row"><span>${bankHolderLabel}</span><span>${bankverbindung.kontoinhaber}</span></div>
<div class="bank-row"><span>${bankIbanLabel}</span><span>${bankverbindung.iban}</span></div>
${bankverbindung.bic ? `<div class="bank-row"><span>${bankBicLabel}</span><span>${bankverbindung.bic}</span></div>` : ""}
${bankverbindung.bank ? `<div class="bank-row"><span>${bankBankNameLabel}</span><span>${bankverbindung.bank}</span></div>` : ""}
`;
} else {
bankDetailsEl.innerHTML = `<p class="small">${bankPlaceholderLabel}</p>`;
}
}
document.getElementById("coupon-apply").addEventListener("click", () => {
const input = document.getElementById("coupon-input");
const code = input.value.trim();
if (!code) { clearCoupon(); renderSummary(); return; }
setCouponCode(code);
input.dataset.invalid = appliedCoupon() ? "0" : "1";
renderSummary();
});
document.getElementById("partner-code-apply").addEventListener("click", () => {
const input = document.getElementById("partner-code-input");
const code = input.value.trim();
if (!code) { clearPartnerCode(); renderSummary(); return; }
setPartnerCode(code);
input.dataset.invalid = appliedPartnerCode() ? "0" : "1";
renderSummary();
});
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;
}
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;
}
});
</script>