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]>
407 lines
24 KiB
Plaintext
407 lines
24 KiB
Plaintext
---
|
||
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 l’Exemple" 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¤cy=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>
|