Zahlungsarten überarbeitet: echtes PayPal, Klarna & eigene Kartenmaske raus
Auf ausdrücklichen Wunsch, mit Fokus auf rechtliche Absicherung: - Klarna komplett entfernt (hätte eigene Händlerprüfung + Bonitätsprüfungs- Pflichten nach der neuen EU-Verbraucherkreditrichtlinie vorausgesetzt). - Eigene Kreditkarten-Eingabemaske komplett entfernt (wäre PCI-DSS-pflichtig gewesen — für einen kleinen Shop praktisch nicht stemmbar). Kartenzahlung bleibt möglich: PayPals eigener, PCI-zertifizierter Gast-Checkout bietet Kredit-/Debitkarte an, ohne dass Kartendaten je unsere Seite berühren. - PayPal ist jetzt ECHT server-seitig verifiziert statt dem Browser blind zu vertrauen: eigener Cloudflare-Function-Flow (functions/_shared/paypal.js + functions/api/paypal/) legt die PayPal-Bestellung server-seitig an, zieht die Zahlung nach Bestätigung server-seitig ein und prüft den eingezogenen Betrag gegen die Bestellsumme — die Bestellung wird ausschließlich bei bestätigter, betragsgleicher Zahlung angelegt. /api/orders lehnt direkte PayPal-Bestellungen jetzt ausdrücklich ab (verhindert vorgetäuschte "bezahlte" Bestellungen ohne echte Zahlung). - Neuer, ehrlicherer Bestellstatus "zahlungOffen": Überweisungs-Bestellungen starten jetzt so (Geld noch nicht da) statt fälschlich sofort "bezahlt" zu heißen — Schutz vor Warenversand, bevor das Geld wirklich angekommen ist. Eigene Kachel/Filter/Badge-Farbe in der Verwaltung, Umsatz-/Auswertungs- Zahlen zählen "zahlungOffen" bewusst nicht mit. - Rechtstexte (AGB, Datenschutzerklärung, FAQ, Versand & Zahlung) auf allen 4 Sprachen aktualisiert: nur noch PayPal + Überweisung erwähnt, inkl. DSGVO- Hinweis zur internationalen Datenübertragung an PayPal (Data Privacy Framework-Zertifizierung). - D1-Migration 0003: neue Spalte paypal_order_id (Zahlungsbeleg) + erweiterter Status-Wertebereich, auf Live-Datenbank angewendet, bestehende Daten intakt. Mit echten Testbestellungen lokal verifiziert: Überweisung legt korrekt "zahlungOffen" an, direkter PayPal-Bypass-Versuch an /api/orders wird abgelehnt, PayPal-Route meldet sauber "noch nicht eingerichtet" ohne Zugangsdaten. Checkout-UI zeigt nur noch 2 Zahlungsarten, Bestellknopf ist bei PayPal ausgeblendet (Zahlung läuft exklusiv über den echten PayPal-Button). Für den echten Zahlungseingang fehlt noch VanVans eigenes PayPal-Business- Konto (Client-ID + Secret) — Details in der Vault-Dokumentation. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
98028df4ea
commit
81c175d03c
+107
-126
@@ -6,7 +6,7 @@ import { useTranslations } from "../../../i18n/ui";
|
||||
const lang: Locale = "ch";
|
||||
const t = useTranslations(lang);
|
||||
---
|
||||
<Layout title="Checkout" description="Bschtellig abschliesse bi Van's DIY & Bastelbedarf." lang={lang} path="/checkout/">
|
||||
<Layout title="Checkout" description="Bstellig abschliesse bi Van's DIY & Bastelbedarf." lang={lang} path="/checkout/">
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<span class="eyebrow">{t.checkout.eyebrow}</span>
|
||||
@@ -20,65 +20,22 @@ const t = useTranslations(lang);
|
||||
<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). */}
|
||||
{/* Nur noch zwei Zahlungsarten: Klarna raus (Bonitätsprüfungs-Pflichten), keine eigene
|
||||
Kreditkarten-Eingabemaske (PCI-DSS-pflichtig) — Kartenzahlung läuft stattdessen über
|
||||
PayPals eigenen, PCI-zertifizierten Gast-Checkout. */}
|
||||
<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>
|
||||
<span>PayPal<span class="pay-sub">{t.checkout.payPalSub}<span class="pay-sub-cards" aria-hidden="true"><span class="mini-card mini-card-visa"><svg viewBox="0 0 24 24" 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" 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></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>
|
||||
<p class="pay-connect-status" id="paypal-error" style="display:none; color: var(--c-sale);"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-card" style="--brand-color:#7fa8c9; --brand-bg:#3b5a76;">
|
||||
@@ -137,7 +94,8 @@ const t = useTranslations(lang);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
|
||||
<p id="order-submit-hint">{t.checkout.payPalCompleteHint}</p>
|
||||
<button class="btn btn-primary btn-block" type="submit" id="order-submit-btn">{t.checkout.orderButton}</button>
|
||||
</form>
|
||||
|
||||
<aside class="card checkout-aside">
|
||||
@@ -173,9 +131,9 @@ const t = useTranslations(lang);
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/ch/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 }}>
|
||||
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/ch/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, 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, payPalCompleteHintLabel: t.checkout.payPalCompleteHint }}>
|
||||
// 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 };
|
||||
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel };
|
||||
</script>
|
||||
<script type="module">
|
||||
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
|
||||
@@ -185,12 +143,15 @@ const t = useTranslations(lang);
|
||||
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 { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel } = window.__checkoutVars;
|
||||
|
||||
// TODO (VanVan/qciga): "sb" isch PayPals gratis Sandbox-Test-ID — funktioniert sofort ohni
|
||||
// eigenes Konto, aber es flüsst nie echts Gäld. Für echte Zahligsigang hie die eigeti PayPal-
|
||||
// Business-Client-ID iitrage (gratis uf developer.paypal.com).
|
||||
const PAYPAL_CLIENT_ID = "sb";
|
||||
|
||||
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;
|
||||
@@ -250,59 +211,103 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
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.
|
||||
function baueBestellPayload() {
|
||||
return {
|
||||
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(),
|
||||
},
|
||||
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,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function nachBestellErfolg() {
|
||||
if (treuebonusDiscount() > 0) {
|
||||
treuebonusEinloesen();
|
||||
} else {
|
||||
registriereAbgeschlosseneBestellung();
|
||||
}
|
||||
location.href = thankYouPath;
|
||||
}
|
||||
|
||||
// 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;
|
||||
const paypalErrorEl = document.getElementById("paypal-error");
|
||||
function zeigePaypalFehler(msg) {
|
||||
if (!paypalErrorEl) return;
|
||||
paypalErrorEl.textContent = "⚠️ " + msg;
|
||||
paypalErrorEl.style.display = "block";
|
||||
}
|
||||
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.src = `https://www.paypal.com/sdk/js?client-id=${PAYPAL_CLIENT_ID}¤cy=EUR&intent=capture`;
|
||||
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; }),
|
||||
createOrder: async () => {
|
||||
if (cart.length === 0) {
|
||||
alert(emptyCartAlert);
|
||||
throw new Error("Warenkorb leer");
|
||||
}
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
const res = await fetch("/api/paypal/create-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ summe: currentTotal }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok || !data?.ok) {
|
||||
zeigePaypalFehler(data?.error || "PayPal-Bestellung konnte nicht angelegt werden.");
|
||||
throw new Error(data?.error || "PayPal-Bestellung fehlgeschlagen.");
|
||||
}
|
||||
return data.id;
|
||||
},
|
||||
onApprove: async (data) => {
|
||||
try {
|
||||
const res = await fetch("/api/paypal/capture-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ paypalOrderId: data.orderID, ...baueBestellPayload() }),
|
||||
});
|
||||
const result = await res.json().catch(() => null);
|
||||
if (!res.ok || !result?.ok) {
|
||||
zeigePaypalFehler(result?.error || "Zahlung konnte nicht abgeschlossen werden.");
|
||||
return;
|
||||
}
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
zeigePaypalFehler(err instanceof Error ? err.message : "Unbekannter Fehler bei der Zahlung.");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
zeigePaypalFehler("PayPal hat einen Fehler gemeldet. Bitte versuche es erneut oder wähle Überweisung.");
|
||||
},
|
||||
onCancel: () => {
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
},
|
||||
}).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)) {
|
||||
@@ -336,7 +341,7 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
});
|
||||
|
||||
const orderForm = document.getElementById("checkout-form");
|
||||
const submitBtn = orderForm.querySelector('button[type="submit"]');
|
||||
const submitBtn = document.getElementById("order-submit-btn");
|
||||
const orderErrorEl = document.createElement("p");
|
||||
orderErrorEl.className = "small";
|
||||
orderErrorEl.style.color = "var(--c-sale)";
|
||||
@@ -350,53 +355,29 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
return;
|
||||
}
|
||||
|
||||
const zahlungsart = document.querySelector('input[name="pay"]:checked')?.value || "";
|
||||
if (zahlungsart === "paypal") {
|
||||
orderErrorEl.textContent = "⚠️ " + payPalCompleteHintLabel;
|
||||
orderErrorEl.style.display = "block";
|
||||
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,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/orders", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({ ...baueBestellPayload(), zahlungsart }),
|
||||
});
|
||||
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;
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
orderErrorEl.textContent = "⚠️ " + (err instanceof Error ? err.message : "Bestellung konnte nicht gespeichert werden. Bitte nochmal versuchen.");
|
||||
orderErrorEl.style.display = "block";
|
||||
|
||||
+144
-131
@@ -20,65 +20,26 @@ const t = useTranslations(lang);
|
||||
<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). */}
|
||||
{/* Nur noch zwei Zahlungsarten (auf ausdrücklichen Wunsch, siehe Recherche zu Rechts-/
|
||||
Kostenlage): Klarna raus (würde eine eigene Händlerprüfung + Bonitätsprüfungs-
|
||||
Pflichten voraussetzen, siehe EU-Verbraucherkreditrichtlinie) und keine eigene
|
||||
Kreditkarten-Eingabemaske mehr (wäre PCI-DSS-pflichtig — für einen kleinen Shop
|
||||
praktisch nicht stemmbar). Kartenzahlung ist trotzdem möglich: PayPal bietet als
|
||||
Gast-Checkout auch Kredit-/Debitkarte an, komplett auf PayPals eigener, PCI-
|
||||
zertifizierter Seite — die Kartendaten berühren unsere Seite nie. */}
|
||||
<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>
|
||||
<span>PayPal<span class="pay-sub">{t.checkout.payPalSub}<span class="pay-sub-cards" aria-hidden="true"><span class="mini-card mini-card-visa"><svg viewBox="0 0 24 24" 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" 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></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>
|
||||
<p class="pay-connect-status" id="paypal-error" style="display:none; color: var(--c-sale);"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-card" style="--brand-color:#7fa8c9; --brand-bg:#3b5a76;">
|
||||
@@ -137,7 +98,11 @@ const t = useTranslations(lang);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
|
||||
{/* Dieser Knopf gehört NUR zur Überweisung — bei ausgewähltem PayPal wird er per CSS
|
||||
(siehe global.css, #checkout-form:has(...)) ausgeblendet und stattdessen der Hinweis
|
||||
darunter gezeigt, dass die Zahlung über den echten PayPal-Button oben läuft. */}
|
||||
<p id="order-submit-hint">{t.checkout.payPalCompleteHint}</p>
|
||||
<button class="btn btn-primary btn-block" type="submit" id="order-submit-btn">{t.checkout.orderButton}</button>
|
||||
</form>
|
||||
|
||||
<aside class="card checkout-aside">
|
||||
@@ -173,12 +138,12 @@ const t = useTranslations(lang);
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/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 }}>
|
||||
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, 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, payPalCompleteHintLabel: t.checkout.payPalCompleteHint }}>
|
||||
// WICHTIG: define:vars-Skripte werden von Astro in ein IIFE gepackt (kein ES-Modul) — echte
|
||||
// `import`-Anweisungen würden hier zur Laufzeit mit "Cannot use import statement outside a
|
||||
// module" fehlschlagen. Deshalb hier nur die vom Server gerenderten Werte auf window ablegen,
|
||||
// die eigentliche Logik läuft im separaten <script type="module"> darunter.
|
||||
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 };
|
||||
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel };
|
||||
</script>
|
||||
<script type="module">
|
||||
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../scripts/cart";
|
||||
@@ -188,7 +153,15 @@ const t = useTranslations(lang);
|
||||
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 { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel } = window.__checkoutVars;
|
||||
|
||||
// TODO (VanVan/qciga): "sb" ist PayPals kostenlose Sandbox-Test-ID — funktioniert sofort, ohne
|
||||
// eigenes Konto, aber es fließt dabei nie echtes Geld. Für echten Zahlungseingang hier die
|
||||
// eigene PayPal-Business-Client-ID eintragen (kostenlos unter developer.paypal.com, siehe
|
||||
// Vault-Doku "Zugänge & Zugangscodes"). Die Client-ID ist NICHT geheim (anders als das
|
||||
// zugehörige Secret, das ausschließlich server-seitig als Cloudflare-Secret hinterlegt wird,
|
||||
// siehe functions/_shared/paypal.js) — sie darf hier im Quellcode stehen.
|
||||
const PAYPAL_CLIENT_ID = "sb";
|
||||
|
||||
const summary = document.getElementById("checkout-summary");
|
||||
const landSelect = document.getElementById("land");
|
||||
@@ -255,59 +228,126 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
renderSummary();
|
||||
landSelect.addEventListener("change", renderSummary);
|
||||
|
||||
// Zahlungsart-Auswahl: der "Verbinden"-Bereich jeder Karte klappt rein über CSS
|
||||
// Zahlungsart-Auswahl: der Panel-Bereich jeder Karte klappt rein über CSS
|
||||
// (:has(input:checked)) sofort auf, sobald man die Karte anklickt — kein JS, keine Verzögerung.
|
||||
// Ebenso blendet CSS den großen "Jetzt bestellen"-Knopf aus, sobald PayPal gewählt ist (siehe
|
||||
// global.css) — die Zahlung läuft dann ausschließlich über den PayPal-Button unten.
|
||||
|
||||
// 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.
|
||||
// Baut das Bestell-Objekt, das sowohl an /api/orders (Überweisung) als auch an
|
||||
// /api/paypal/capture-order (PayPal, nach bestätigter Zahlung) geschickt wird — identisches
|
||||
// Format, damit beide Wege dieselbe Server-Logik (functions/_shared/bestellung-erstellen.js)
|
||||
// nutzen können.
|
||||
function baueBestellPayload() {
|
||||
return {
|
||||
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(),
|
||||
},
|
||||
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,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// Läuft nach JEDER erfolgreich gespeicherten Bestellung, egal ob per Überweisung oder PayPal.
|
||||
function nachBestellErfolg() {
|
||||
// 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();
|
||||
}
|
||||
location.href = thankYouPath;
|
||||
}
|
||||
|
||||
// PayPal: ECHTE, server-seitig verifizierte Zahlungsabwicklung (siehe functions/api/paypal/ +
|
||||
// functions/_shared/paypal.js). Der Browser vertraut NICHT mehr blind seinem eigenen
|
||||
// "capture()"-Aufruf — createOrder lässt UNSEREN Server die PayPal-Bestellung anlegen (mit dem
|
||||
// von uns berechneten Betrag), onApprove lässt UNSEREN Server die Zahlung bei PayPal
|
||||
// einziehen/prüfen und ERST DANACH die Bestellung speichern.
|
||||
let paypalLoaded = false;
|
||||
const paypalErrorEl = document.getElementById("paypal-error");
|
||||
function zeigePaypalFehler(msg) {
|
||||
if (!paypalErrorEl) return;
|
||||
paypalErrorEl.textContent = "⚠️ " + msg;
|
||||
paypalErrorEl.style.display = "block";
|
||||
}
|
||||
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";
|
||||
// disable-funding NICHT gesetzt: PayPals Gast-Checkout mit Kredit-/Debitkarte bleibt bewusst
|
||||
// verfügbar — das ist die sichere Alternative zu einer eigenen Kartenmaske (siehe oben).
|
||||
script.src = `https://www.paypal.com/sdk/js?client-id=${PAYPAL_CLIENT_ID}¤cy=EUR&intent=capture`;
|
||||
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; }),
|
||||
createOrder: async () => {
|
||||
if (cart.length === 0) {
|
||||
alert(emptyCartAlert);
|
||||
throw new Error("Warenkorb leer");
|
||||
}
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
const res = await fetch("/api/paypal/create-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ summe: currentTotal }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok || !data?.ok) {
|
||||
zeigePaypalFehler(data?.error || "PayPal-Bestellung konnte nicht angelegt werden.");
|
||||
throw new Error(data?.error || "PayPal-Bestellung fehlgeschlagen.");
|
||||
}
|
||||
return data.id;
|
||||
},
|
||||
onApprove: async (data) => {
|
||||
try {
|
||||
const res = await fetch("/api/paypal/capture-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ paypalOrderId: data.orderID, ...baueBestellPayload() }),
|
||||
});
|
||||
const result = await res.json().catch(() => null);
|
||||
if (!res.ok || !result?.ok) {
|
||||
zeigePaypalFehler(result?.error || "Zahlung konnte nicht abgeschlossen werden.");
|
||||
return;
|
||||
}
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
zeigePaypalFehler(err instanceof Error ? err.message : "Unbekannter Fehler bei der Zahlung.");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
zeigePaypalFehler("PayPal hat einen Fehler gemeldet. Bitte versuche es erneut oder wähle Überweisung.");
|
||||
},
|
||||
onCancel: () => {
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
},
|
||||
}).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.
|
||||
// im Adminbereich hinterlegte Kontodaten an. Die Bestellung wird erst nach Zahlungseingang
|
||||
// manuell in /verwaltung/ von "Zahlung ausstehend" auf "Bezahlt" gestellt.
|
||||
const bankDetailsEl = document.getElementById("bank-details");
|
||||
if (bankDetailsEl) {
|
||||
if (bankverbindungVollstaendig(bankverbindung)) {
|
||||
@@ -341,13 +381,16 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
});
|
||||
|
||||
const orderForm = document.getElementById("checkout-form");
|
||||
const submitBtn = orderForm.querySelector('button[type="submit"]');
|
||||
const submitBtn = document.getElementById("order-submit-btn");
|
||||
const orderErrorEl = document.createElement("p");
|
||||
orderErrorEl.className = "small";
|
||||
orderErrorEl.style.color = "var(--c-sale)";
|
||||
orderErrorEl.style.display = "none";
|
||||
submitBtn.insertAdjacentElement("beforebegin", orderErrorEl);
|
||||
|
||||
// Dieser Handler ist jetzt AUSSCHLIESSLICH für Überweisung zuständig — der Knopf ist bei
|
||||
// ausgewähltem PayPal per CSS unsichtbar, hier trotzdem ein zusätzliches Sicherheitsnetz
|
||||
// (z.B. Absenden per Enter-Taste in einem Textfeld), falls PayPal doch ausgewählt sein sollte.
|
||||
orderForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
if (cart.length === 0) {
|
||||
@@ -355,63 +398,33 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.
|
||||
const zahlungsart = document.querySelector('input[name="pay"]:checked')?.value || "";
|
||||
if (zahlungsart === "paypal") {
|
||||
orderErrorEl.textContent = "⚠️ " + payPalCompleteHintLabel;
|
||||
orderErrorEl.style.display = "block";
|
||||
return;
|
||||
}
|
||||
|
||||
// Echte Bestellung in der Datenbank anlegen (siehe functions/api/orders.js) — Status
|
||||
// "zahlungOffen", da bei Überweisung das Geld noch nicht da ist. 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 nichts 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,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/orders", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({ ...baueBestellPayload(), zahlungsart }),
|
||||
});
|
||||
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;
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
orderErrorEl.textContent = "⚠️ " + (err instanceof Error ? err.message : "Bestellung konnte nicht gespeichert werden. Bitte nochmal versuchen.");
|
||||
orderErrorEl.style.display = "block";
|
||||
|
||||
+110
-125
@@ -20,65 +20,22 @@ const t = useTranslations(lang);
|
||||
<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). */}
|
||||
{/* Nur noch zwei Zahlungsarten: Klarna raus (Bonitätsprüfungs-Pflichten), keine eigene
|
||||
Kreditkarten-Eingabemaske (PCI-DSS-pflichtig) — Kartenzahlung läuft stattdessen über
|
||||
PayPals eigenen, PCI-zertifizierten Gast-Checkout. */}
|
||||
<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>
|
||||
<span>PayPal<span class="pay-sub">{t.checkout.payPalSub}<span class="pay-sub-cards" aria-hidden="true"><span class="mini-card mini-card-visa"><svg viewBox="0 0 24 24" 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" 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></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>
|
||||
<p class="pay-connect-status" id="paypal-error" style="display:none; color: var(--c-sale);"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-card" style="--brand-color:#7fa8c9; --brand-bg:#3b5a76;">
|
||||
@@ -137,7 +94,8 @@ const t = useTranslations(lang);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
|
||||
<p id="order-submit-hint">{t.checkout.payPalCompleteHint}</p>
|
||||
<button class="btn btn-primary btn-block" type="submit" id="order-submit-btn">{t.checkout.orderButton}</button>
|
||||
</form>
|
||||
|
||||
<aside class="card checkout-aside">
|
||||
@@ -173,9 +131,9 @@ const t = useTranslations(lang);
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/en/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 }}>
|
||||
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/en/checkout/danke/", checkoutLang: lang, discountLabel: t.cart.discount, couponInvalid: t.cart.couponInvalid, couponAppliedTemplate: t.cart.couponApplied("__C__"), couponRemoveLabel: t.cart.couponRemove, 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, payPalCompleteHintLabel: t.checkout.payPalCompleteHint }}>
|
||||
// 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 };
|
||||
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel };
|
||||
</script>
|
||||
<script type="module">
|
||||
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
|
||||
@@ -185,12 +143,16 @@ const t = useTranslations(lang);
|
||||
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 { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel } = window.__checkoutVars;
|
||||
|
||||
// TODO (VanVan/qciga): "sb" is PayPal's free sandbox test ID — works immediately without an
|
||||
// account, but no real money moves. Replace with VanVan's own (free) PayPal Business client ID
|
||||
// once ready (developer.paypal.com). The client ID itself is NOT secret (unlike the matching
|
||||
// secret, which stays server-side only, see functions/_shared/paypal.js).
|
||||
const PAYPAL_CLIENT_ID = "sb";
|
||||
|
||||
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;
|
||||
@@ -250,59 +212,106 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
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.
|
||||
function baueBestellPayload() {
|
||||
return {
|
||||
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(),
|
||||
},
|
||||
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,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// 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.
|
||||
function nachBestellErfolg() {
|
||||
if (treuebonusDiscount() > 0) {
|
||||
treuebonusEinloesen();
|
||||
} else {
|
||||
registriereAbgeschlosseneBestellung();
|
||||
}
|
||||
location.href = thankYouPath;
|
||||
}
|
||||
|
||||
// PayPal: real, server-verified payment flow (see functions/api/paypal/ +
|
||||
// functions/_shared/paypal.js) — createOrder/onApprove both call our own server, which talks
|
||||
// to PayPal directly, instead of trusting the browser's own capture() result.
|
||||
let paypalLoaded = false;
|
||||
const paypalErrorEl = document.getElementById("paypal-error");
|
||||
function zeigePaypalFehler(msg) {
|
||||
if (!paypalErrorEl) return;
|
||||
paypalErrorEl.textContent = "⚠️ " + msg;
|
||||
paypalErrorEl.style.display = "block";
|
||||
}
|
||||
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.src = `https://www.paypal.com/sdk/js?client-id=${PAYPAL_CLIENT_ID}¤cy=EUR&intent=capture`;
|
||||
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; }),
|
||||
createOrder: async () => {
|
||||
if (cart.length === 0) {
|
||||
alert(emptyCartAlert);
|
||||
throw new Error("Cart empty");
|
||||
}
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
const res = await fetch("/api/paypal/create-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ summe: currentTotal }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok || !data?.ok) {
|
||||
zeigePaypalFehler(data?.error || "Could not create PayPal order.");
|
||||
throw new Error(data?.error || "PayPal order failed.");
|
||||
}
|
||||
return data.id;
|
||||
},
|
||||
onApprove: async (data) => {
|
||||
try {
|
||||
const res = await fetch("/api/paypal/capture-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ paypalOrderId: data.orderID, ...baueBestellPayload() }),
|
||||
});
|
||||
const result = await res.json().catch(() => null);
|
||||
if (!res.ok || !result?.ok) {
|
||||
zeigePaypalFehler(result?.error || "Payment could not be completed.");
|
||||
return;
|
||||
}
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
zeigePaypalFehler(err instanceof Error ? err.message : "Unknown payment error.");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
zeigePaypalFehler("PayPal reported an error. Please try again or choose bank transfer.");
|
||||
},
|
||||
onCancel: () => {
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
},
|
||||
}).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)) {
|
||||
@@ -336,7 +345,7 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
});
|
||||
|
||||
const orderForm = document.getElementById("checkout-form");
|
||||
const submitBtn = orderForm.querySelector('button[type="submit"]');
|
||||
const submitBtn = document.getElementById("order-submit-btn");
|
||||
const orderErrorEl = document.createElement("p");
|
||||
orderErrorEl.className = "small";
|
||||
orderErrorEl.style.color = "var(--c-sale)";
|
||||
@@ -350,53 +359,29 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
return;
|
||||
}
|
||||
|
||||
const zahlungsart = document.querySelector('input[name="pay"]:checked')?.value || "";
|
||||
if (zahlungsart === "paypal") {
|
||||
orderErrorEl.textContent = "⚠️ " + payPalCompleteHintLabel;
|
||||
orderErrorEl.style.display = "block";
|
||||
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,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/orders", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({ ...baueBestellPayload(), zahlungsart }),
|
||||
});
|
||||
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;
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
orderErrorEl.textContent = "⚠️ " + (err instanceof Error ? err.message : "Bestellung konnte nicht gespeichert werden. Bitte nochmal versuchen.");
|
||||
orderErrorEl.style.display = "block";
|
||||
|
||||
+106
-125
@@ -20,65 +20,22 @@ const t = useTranslations(lang);
|
||||
<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). */}
|
||||
{/* Nur noch zwei Zahlungsarten: Klarna raus (Bonitätsprüfungs-Pflichten), keine eigene
|
||||
Kreditkarten-Eingabemaske (PCI-DSS-pflichtig) — Kartenzahlung läuft stattdessen über
|
||||
PayPals eigenen, PCI-zertifizierten Gast-Checkout. */}
|
||||
<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>
|
||||
<span>PayPal<span class="pay-sub">{t.checkout.payPalSub}<span class="pay-sub-cards" aria-hidden="true"><span class="mini-card mini-card-visa"><svg viewBox="0 0 24 24" 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" 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></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>
|
||||
<p class="pay-connect-status" id="paypal-error" style="display:none; color: var(--c-sale);"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pay-card" style="--brand-color:#7fa8c9; --brand-bg:#3b5a76;">
|
||||
@@ -137,7 +94,8 @@ const t = useTranslations(lang);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
|
||||
<p id="order-submit-hint">{t.checkout.payPalCompleteHint}</p>
|
||||
<button class="btn btn-primary btn-block" type="submit" id="order-submit-btn">{t.checkout.orderButton}</button>
|
||||
</form>
|
||||
|
||||
<aside class="card checkout-aside">
|
||||
@@ -173,9 +131,9 @@ const t = useTranslations(lang);
|
||||
</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 }}>
|
||||
<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, 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, payPalCompleteHintLabel: t.checkout.payPalCompleteHint }}>
|
||||
// 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 };
|
||||
window.__checkoutVars = { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel };
|
||||
</script>
|
||||
<script type="module">
|
||||
import { getCart, cartTotal, appliedCoupon, couponDiscount, setCouponCode, clearCoupon, appliedPartnerCode, partnerCodeDiscount, setPartnerCode, clearPartnerCode, totalDiscount, treuebonusDiscount, aboDiscount, istAusgewaehlt, zeilenpreisFuer } from "../../../scripts/cart";
|
||||
@@ -185,12 +143,15 @@ const t = useTranslations(lang);
|
||||
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 { freeLabel, totalLabel, shippingLabel, emptyCartAlert, thankYouPath, checkoutLang, discountLabel, couponInvalid, couponAppliedTemplate, couponRemoveLabel, bankHolderLabel, bankIbanLabel, bankBicLabel, bankBankNameLabel, bankPlaceholderLabel, partnerDiscountLabel, partnerCodeInvalid, partnerCodeAppliedTemplate, partnerCodeRemoveLabel, loyaltyDiscountLabel, aboDiscountLabel, gratisPraemieLabel, payPalCompleteHintLabel } = window.__checkoutVars;
|
||||
|
||||
// TODO (VanVan/qciga) : "sb" est l'identifiant sandbox gratuit de PayPal — fonctionne
|
||||
// immédiatement sans compte, mais aucun argent réel ne circule. Remplacer par le vrai
|
||||
// identifiant client PayPal Business de VanVan une fois prêt (developer.paypal.com).
|
||||
const PAYPAL_CLIENT_ID = "sb";
|
||||
|
||||
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;
|
||||
@@ -250,59 +211,103 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
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.
|
||||
function baueBestellPayload() {
|
||||
return {
|
||||
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(),
|
||||
},
|
||||
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,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function nachBestellErfolg() {
|
||||
if (treuebonusDiscount() > 0) {
|
||||
treuebonusEinloesen();
|
||||
} else {
|
||||
registriereAbgeschlosseneBestellung();
|
||||
}
|
||||
location.href = thankYouPath;
|
||||
}
|
||||
|
||||
// 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;
|
||||
const paypalErrorEl = document.getElementById("paypal-error");
|
||||
function zeigePaypalFehler(msg) {
|
||||
if (!paypalErrorEl) return;
|
||||
paypalErrorEl.textContent = "⚠️ " + msg;
|
||||
paypalErrorEl.style.display = "block";
|
||||
}
|
||||
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.src = `https://www.paypal.com/sdk/js?client-id=${PAYPAL_CLIENT_ID}¤cy=EUR&intent=capture`;
|
||||
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; }),
|
||||
createOrder: async () => {
|
||||
if (cart.length === 0) {
|
||||
alert(emptyCartAlert);
|
||||
throw new Error("Panier vide");
|
||||
}
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
const res = await fetch("/api/paypal/create-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ summe: currentTotal }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok || !data?.ok) {
|
||||
zeigePaypalFehler(data?.error || "Impossible de créer la commande PayPal.");
|
||||
throw new Error(data?.error || "Échec de la commande PayPal.");
|
||||
}
|
||||
return data.id;
|
||||
},
|
||||
onApprove: async (data) => {
|
||||
try {
|
||||
const res = await fetch("/api/paypal/capture-order", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ paypalOrderId: data.orderID, ...baueBestellPayload() }),
|
||||
});
|
||||
const result = await res.json().catch(() => null);
|
||||
if (!res.ok || !result?.ok) {
|
||||
zeigePaypalFehler(result?.error || "Le paiement n'a pas pu être finalisé.");
|
||||
return;
|
||||
}
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
zeigePaypalFehler(err instanceof Error ? err.message : "Erreur de paiement inconnue.");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
zeigePaypalFehler("PayPal a signalé une erreur. Réessayez ou choisissez le virement bancaire.");
|
||||
},
|
||||
onCancel: () => {
|
||||
if (paypalErrorEl) paypalErrorEl.style.display = "none";
|
||||
},
|
||||
}).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)) {
|
||||
@@ -336,7 +341,7 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
});
|
||||
|
||||
const orderForm = document.getElementById("checkout-form");
|
||||
const submitBtn = orderForm.querySelector('button[type="submit"]');
|
||||
const submitBtn = document.getElementById("order-submit-btn");
|
||||
const orderErrorEl = document.createElement("p");
|
||||
orderErrorEl.className = "small";
|
||||
orderErrorEl.style.color = "var(--c-sale)";
|
||||
@@ -350,53 +355,29 @@ import { registriereAbgeschlosseneBestellung, treuebonusEinloesen } from "../../
|
||||
return;
|
||||
}
|
||||
|
||||
const zahlungsart = document.querySelector('input[name="pay"]:checked')?.value || "";
|
||||
if (zahlungsart === "paypal") {
|
||||
orderErrorEl.textContent = "⚠️ " + payPalCompleteHintLabel;
|
||||
orderErrorEl.style.display = "block";
|
||||
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,
|
||||
gutscheinCode: appliedCoupon()?.code ?? null,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/orders", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({ ...baueBestellPayload(), zahlungsart }),
|
||||
});
|
||||
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;
|
||||
nachBestellErfolg();
|
||||
} catch (err) {
|
||||
orderErrorEl.textContent = "⚠️ " + (err instanceof Error ? err.message : "Bestellung konnte nicht gespeichert werden. Bitte nochmal versuchen.");
|
||||
orderErrorEl.style.display = "block";
|
||||
|
||||
@@ -74,6 +74,14 @@ const lang: Locale = "de";
|
||||
</div>
|
||||
<div class="verwaltung-box-body">
|
||||
<div class="verwaltung-stat-grid">
|
||||
{/* Neu seit der echten PayPal-Anbindung: Überweisungs-Bestellungen starten mit
|
||||
Status "zahlungOffen" (Geld noch nicht da) statt fälschlich sofort "bezahlt" zu
|
||||
heißen — eigene Kachel, damit sofort auffällt, worauf noch gewartet wird. */}
|
||||
<button type="button" class="card verwaltung-stat-tile" data-filter="zahlungOffen">
|
||||
<span class="verwaltung-stat-icon" aria-hidden="true">⏳</span>
|
||||
<span class="verwaltung-stat-value" id="stat-zahlungOffen">0</span>
|
||||
<span class="verwaltung-stat-label">Zahlung ausstehend</span>
|
||||
</button>
|
||||
<button type="button" class="card verwaltung-stat-tile" data-filter="offen">
|
||||
<span class="verwaltung-stat-icon" aria-hidden="true">🆕</span>
|
||||
<span class="verwaltung-stat-value" id="stat-offen">0</span>
|
||||
@@ -259,6 +267,7 @@ const lang: Locale = "de";
|
||||
|
||||
<nav class="verwaltung-filter" aria-label="Nach Status filtern">
|
||||
<button type="button" class="verwaltung-filter-btn is-active" data-filter="alle">Alle (<span id="filter-count-alle">0</span>)</button>
|
||||
<button type="button" class="verwaltung-filter-btn" data-filter="zahlungOffen">Zahlung ausstehend (<span id="filter-count-zahlungOffen">0</span>)</button>
|
||||
<button type="button" class="verwaltung-filter-btn" data-filter="offen">Offen (<span id="filter-count-offen">0</span>)</button>
|
||||
<button type="button" class="verwaltung-filter-btn" data-filter="inBearbeitung">In Bearbeitung (<span id="filter-count-inBearbeitung">0</span>)</button>
|
||||
<button type="button" class="verwaltung-filter-btn" data-filter="versendet">Versendet (<span id="filter-count-versendet">0</span>)</button>
|
||||
@@ -288,6 +297,7 @@ const lang: Locale = "de";
|
||||
|
||||
const lang = "de";
|
||||
const STATUS_LABEL = {
|
||||
zahlungOffen: "Zahlung ausstehend",
|
||||
bezahlt: "Bezahlt",
|
||||
bearbeitung: "In Bearbeitung",
|
||||
versandVorbereitet: "Versand wird vorbereitet",
|
||||
@@ -297,7 +307,11 @@ const lang: Locale = "de";
|
||||
};
|
||||
const LAND_NAME = { de: "Deutschland", at: "Österreich", ch: "Schweiz", lu: "Luxemburg" };
|
||||
const LAND_FLAGGE = { de: "🇩🇪", at: "🇦🇹", ch: "🇨🇭", lu: "🇱🇺" };
|
||||
const ZAHLUNGSART_LABEL = { paypal: "💙 PayPal", klarna: "🩷 Klarna", kreditkarte: "💳 Kreditkarte", ueberweisung: "🏦 Überweisung" };
|
||||
// Nur noch PayPal (echt, server-verifiziert) und Überweisung (Vorkasse) — Klarna und die
|
||||
// frühere eigene Kreditkarten-Eingabemaske wurden auf ausdrücklichen Wunsch entfernt (siehe
|
||||
// Vault-Doku: Klarna bräuchte eine eigene Händlerprüfung + Bonitätsprüfungs-Pflichten, eine
|
||||
// eigene Kartenmaske wäre PCI-DSS-pflichtig gewesen).
|
||||
const ZAHLUNGSART_LABEL = { paypal: "💙 PayPal", ueberweisung: "🏦 Überweisung" };
|
||||
const INVOICE_LABELS = {
|
||||
title: "Rechnung", numberLabel: "Rechnungsnummer", dateLabel: "Rechnungsdatum",
|
||||
sellerLabel: "Verkäufer", billToLabel: "Rechnungsempfänger",
|
||||
@@ -311,6 +325,7 @@ const lang: Locale = "de";
|
||||
|
||||
function bucket(status) {
|
||||
if (status === "storniert") return "storniert";
|
||||
if (status === "zahlungOffen") return "zahlungOffen";
|
||||
if (status === "bezahlt") return "offen";
|
||||
if (status === "bearbeitung" || status === "versandVorbereitet") return "inBearbeitung";
|
||||
if (status === "versendet") return "versendet";
|
||||
@@ -396,7 +411,9 @@ const lang: Locale = "de";
|
||||
}
|
||||
|
||||
function render() {
|
||||
const aktive = alleBestellungen.filter((b) => b.status !== "storniert");
|
||||
// "aktive" = zählt für Umsatz/Auswertung: weder storniert noch (bei Überweisung) noch gar
|
||||
// nicht bezahlt — sonst würde Geld gezeigt, das VanVan noch gar nicht hat.
|
||||
const aktive = alleBestellungen.filter((b) => b.status !== "storniert" && b.status !== "zahlungOffen");
|
||||
letzteAktive = aktive;
|
||||
|
||||
// Die Auswertungs-Detail-Panels zeigen ggf. veraltete Daten, sobald sich die Bestellliste
|
||||
@@ -408,20 +425,24 @@ const lang: Locale = "de";
|
||||
document.querySelectorAll(".verwaltung-stat-tile.is-active").forEach((t) => t.classList.remove("is-active"));
|
||||
|
||||
// ── Buckets für die Stat-Kacheln + Filterleiste ──
|
||||
const buckets = { offen: 0, inBearbeitung: 0, versendet: 0, abgeschlossen: 0, storniert: 0 };
|
||||
const buckets = { zahlungOffen: 0, offen: 0, inBearbeitung: 0, versendet: 0, abgeschlossen: 0, storniert: 0 };
|
||||
alleBestellungen.forEach((b) => { buckets[bucket(b.status)]++; });
|
||||
setText("stat-zahlungOffen", String(buckets.zahlungOffen));
|
||||
setText("stat-offen", String(buckets.offen));
|
||||
setText("stat-inBearbeitung", String(buckets.inBearbeitung));
|
||||
setText("stat-versendet", String(buckets.versendet));
|
||||
setText("stat-abgeschlossen", String(buckets.abgeschlossen));
|
||||
setText("filter-count-alle", String(alleBestellungen.length));
|
||||
setText("filter-count-zahlungOffen", String(buckets.zahlungOffen));
|
||||
setText("filter-count-offen", String(buckets.offen));
|
||||
setText("filter-count-inBearbeitung", String(buckets.inBearbeitung));
|
||||
setText("filter-count-versendet", String(buckets.versendet));
|
||||
setText("filter-count-abgeschlossen", String(buckets.abgeschlossen));
|
||||
setText("filter-count-storniert", String(buckets.storniert));
|
||||
|
||||
// ── Umsatz + Ø Bestellwert: stornierte Bestellungen zählen bewusst NICHT mit. ──
|
||||
// ── Umsatz + Ø Bestellwert: stornierte UND noch nicht bezahlte (zahlungOffen) Bestellungen
|
||||
// zählen bewusst NICHT mit — sonst würde der Umsatz Geld zeigen, das noch gar nicht da
|
||||
// ist. ──
|
||||
const gesamtumsatz = aktive.reduce((s, b) => s + Number(b.summe), 0);
|
||||
setText("stat-umsatz", formatPrice(gesamtumsatz, lang));
|
||||
setText("stat-umsatz-label", `Gesamtumsatz (${aktive.length} Bestellung${aktive.length === 1 ? "" : "en"})`);
|
||||
@@ -446,8 +467,13 @@ const lang: Locale = "de";
|
||||
`Diesen Monat${monatCount > 0 ? ` (${formatPrice(monatUmsatz, lang)})` : ""}`
|
||||
);
|
||||
|
||||
// Bewusst über ALLE Bestellungen (nicht nur "aktive"): eine seit Tagen unbezahlte
|
||||
// Überweisungs-Bestellung ("zahlungOffen") braucht mindestens genauso dringend Aufmerksamkeit
|
||||
// wie eine liegen gebliebene bezahlte Bestellung — sonst würde sie einfach vergessen.
|
||||
const attentionIds = new Set(
|
||||
aktive.filter((b) => b.status === "bezahlt" && jetzt - new Date(b.created_at).getTime() > ALTER_SCHWELLE_TAGE * MS_PRO_TAG).map((b) => b.id)
|
||||
alleBestellungen
|
||||
.filter((b) => (b.status === "bezahlt" || b.status === "zahlungOffen") && jetzt - new Date(b.created_at).getTime() > ALTER_SCHWELLE_TAGE * MS_PRO_TAG)
|
||||
.map((b) => b.id)
|
||||
);
|
||||
const attentionTile = document.getElementById("stat-attention-tile");
|
||||
if (attentionIds.size > 0) {
|
||||
|
||||
Reference in New Issue
Block a user