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
@@ -14,7 +14,7 @@
|
||||
},
|
||||
{
|
||||
"heading": "§ 4 Zahlung",
|
||||
"body": "Die Zahlung erfolgt wahlweise per PayPal, Klarna, Kreditkarte (Visa & Mastercard) oder Banküberweisung (Vorkasse)."
|
||||
"body": "Die Zahlung erfolgt wahlweise per PayPal (auch als Gast per Kredit-/Debitkarte, ohne eigenes PayPal-Konto) oder Banküberweisung (Vorkasse). Bei Vorkasse wird die Ware erst nach Zahlungseingang versendet."
|
||||
},
|
||||
{
|
||||
"heading": "§ 5 Lieferung",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"heading": "5. Zahlungsdienstleister",
|
||||
"body": "Je nach gewählter Zahlungsart geben wir Daten an die eingesetzten Zahlungsdienstleister weiter (PayPal, Klarna, Kreditkartenanbieter). Es gelten deren jeweils eigene Datenschutzhinweise. Bei Banküberweisung werden Daten an unser kontoführendes Kreditinstitut übermittelt."
|
||||
"body": "Bei Zahlung per PayPal geben wir die für die Zahlungsabwicklung erforderlichen Daten (u. a. Name, Anschrift, Bestellsumme) an die PayPal (Europe) S.à r.l. et Cie, S.C.A., 22-24 Boulevard Royal, L-2449 Luxembourg, weiter. PayPal verarbeitet diese Daten teilweise auch in den USA über die Muttergesellschaft PayPal, Inc.; PayPal ist nach dem EU-US Data Privacy Framework zertifiziert, das ein angemessenes Datenschutzniveau sicherstellt. Es gelten zusätzlich PayPals eigene Datenschutzhinweise (paypal.com/de/webapps/mpp/ua/privacy-full). Bei Banküberweisung werden Daten an unser kontoführendes Kreditinstitut übermittelt."
|
||||
},
|
||||
{
|
||||
"heading": "6. Kontaktformular",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{ "q": "Ab wann ist der Versand kostenlos?", "a": "Das hängt vom Lieferland ab: Deutschland ab 75 €, Österreich und Luxemburg ab 85 €, Schweiz ab 100 € Warenwert – dann versenden wir kostenfrei mit DHL. Darunter richten sich die Versandkosten nach Produkt, Land und Warenwert – Details dazu findest du auf der Seite Versand & Zahlung." },
|
||||
{ "q": "Kann ich Sonderwünsche oder individuelle Anfertigungen bestellen?", "a": "Ja, gerne! Schreib uns einfach über das Kontaktformular – wir schauen, was möglich ist." },
|
||||
{ "q": "Wie pflege ich meine handgemachten Produkte?", "a": "Die genauen Pflegehinweise stehen jeweils auf der Produktseite. Da unsere Stücke handgehäkelt bzw. handgestrickt sind, empfehlen wir grundsätzlich schonende Handwäsche bei niedriger Temperatur, keinen Trockner und liegend an der Luft trocknen, damit Form und Fasern erhalten bleiben." },
|
||||
{ "q": "Welche Zahlungsarten werden akzeptiert?", "a": "PayPal, Klarna, Kreditkarte (Visa & Mastercard) und Banküberweisung (Vorkasse)." },
|
||||
{ "q": "Welche Zahlungsarten werden akzeptiert?", "a": "PayPal (auch als Gast per Kredit-/Debitkarte, ohne eigenes PayPal-Konto) und Banküberweisung (Vorkasse)." },
|
||||
{ "q": "Kann ich meine Bestellung zurückgeben?", "a": "Es gilt das gesetzliche 14-tägige Widerrufsrecht (siehe unsere Widerrufsbelehrung). Bei individuell auf dich zugeschnittenen Sonderanfertigungen kann das Widerrufsrecht gesetzlich ausgeschlossen sein, bei Standardartikeln aus unserem Sortiment gilt es regulär." },
|
||||
{ "q": "Muss ich Umsatzsteuer zahlen?", "a": "Nein. Van's DIY & Bastelbedarf ist Kleinunternehmer gemäß §19 UStG – es wird keine Umsatzsteuer berechnet und ausgewiesen." },
|
||||
{ "q": "Brauche ich ein Kundenkonto zum Bestellen?", "a": "Nein, du kannst auch bequem als Gast bestellen. Ein Konto lohnt sich aber für Bestellhistorie, Wunschliste und Sendungsverfolgung an einem Ort." }
|
||||
@@ -14,7 +14,7 @@
|
||||
{ "q": "From when is shipping free?", "a": "It depends on the delivery country: Germany from €75, Austria and Luxembourg from €85, Switzerland from €100 order value – above that we ship free of charge via DHL. Below that, shipping costs depend on the product, country and order value – details on our Shipping & Payment page." },
|
||||
{ "q": "Can I request custom or personalized items?", "a": "Yes, gladly! Just reach out via the contact form – we'll see what's possible." },
|
||||
{ "q": "How do I care for my handmade items?", "a": "Care instructions are listed on the respective product page. As our pieces are hand-crocheted or hand-knitted, we generally recommend gentle hand-washing at a low temperature, no tumble dryer, and drying flat to preserve shape and fibres." },
|
||||
{ "q": "Which payment methods are accepted?", "a": "PayPal, Klarna, credit card (Visa & Mastercard) and bank transfer (prepayment)." },
|
||||
{ "q": "Which payment methods are accepted?", "a": "PayPal (also as a guest by credit/debit card, no PayPal account needed) and bank transfer (prepayment)." },
|
||||
{ "q": "Can I return my order?", "a": "The statutory 14-day right of withdrawal applies (see our withdrawal notice). For custom-made pieces tailored to you, the right of withdrawal may be excluded by law; for standard items from our range it applies as usual." },
|
||||
{ "q": "Do I have to pay VAT?", "a": "No. Van's DIY & Bastelbedarf is a small business per §19 UStG – no VAT is charged or shown." },
|
||||
{ "q": "Do I need an account to order?", "a": "No, you can conveniently check out as a guest. An account is worthwhile though for order history, wishlist and shipment tracking all in one place." }
|
||||
@@ -24,7 +24,7 @@
|
||||
{ "q": "Ab wenn isch dr Versand gratis?", "a": "Das chunnt uf's Liferland a: Dütschland ab 75 €, Öschterrych und Luxemburg ab 85 €, d'Schwiz ab 100 € Warewärt – drüber schicke mir gratis mit DHL. Drunder richte sich d'Versandchoschte nach Produkt, Land und Warewärt – Details dezue findsch uf dr Syte Versand & Zahlig." },
|
||||
{ "q": "Cha ich Sonderwünsch oder öppis Individuells bstelle?", "a": "Ja, gärn! Schrib eus eifach übers Kontaktformular – mir luege, was geit." },
|
||||
{ "q": "Wie pfläg ich mini vo Hang gmachte Sächeli?", "a": "D'gnaue Pflegehinwys stöh jewils uf dr Produktsyte. Wil eusi Sächeli vo Hang ghäklet oder gstrickt sind, empfähle mir grundsätzlich schonendi Handwösch bi niedriger Temperatur, kei Tumbler und liegend a dr Luft träechne, dass Form und Fase erhalte bliebe." },
|
||||
{ "q": "Weli Zahligsarte werded akzeptiert?", "a": "PayPal, Klarna, Kreditkarte (Visa & Mastercard) und Banküberwysig (Vorusszahlig)." },
|
||||
{ "q": "Weli Zahligsarte werded akzeptiert?", "a": "PayPal (au als Gast mit Kredit-/Debitkarte, ohni eigenes PayPal-Konto) und Banküberwysig (Vorusszahlig)." },
|
||||
{ "q": "Cha ich mini Bschtellig zrugg gäh?", "a": "Es gilt das gsetzlichi 14-tägig Widerrufsrächt (lueg üsi Widerrufsbelehrig aa). Bi Sondranfertigunge, wo speziell uf dich gmacht sind, cha das Widerrufsrächt gsetzlich usgschlosse si, bi Standardartikel us eusem Sortimänt gilt's regulär." },
|
||||
{ "q": "Muess ich Umsatzsteuer zahle?", "a": "Nei. Van's DIY & Bastelbedarf isch Kleinunternehmer gemäss §19 UStG – es wird kei Umsatzsteuer berechnet und usgwiese." },
|
||||
{ "q": "Bruuch ich es Kundekonto zum Bstelle?", "a": "Nei, du chasch au eifach als Gast bstelle. Es Konto lohnt sich aber für d'Bstellhistorie, d'Wunschlischte und d'Sändigsverfolgig a eim Ort." }
|
||||
@@ -34,7 +34,7 @@
|
||||
{ "q": "À partir de quand la livraison est-elle gratuite ?", "a": "Cela dépend du pays de livraison : Allemagne à partir de 75 €, Autriche et Luxembourg à partir de 85 €, Suisse à partir de 100 € d'achat – au-delà, nous livrons gratuitement via DHL. En dessous, les frais de port dépendent du produit, du pays et du montant de la commande – détails sur notre page Livraison & Paiement." },
|
||||
{ "q": "Puis-je commander une pièce personnalisée ?", "a": "Oui, avec plaisir ! Écrivez-nous simplement via le formulaire de contact – nous verrons ce qui est possible." },
|
||||
{ "q": "Comment entretenir mes produits faits main ?", "a": "Les instructions d'entretien figurent sur chaque page produit. Nos pièces étant crochetées ou tricotées à la main, nous recommandons en général un lavage à la main délicat à basse température, sans sèche-linge, et un séchage à plat pour préserver la forme et les fibres." },
|
||||
{ "q": "Quels moyens de paiement sont acceptés ?", "a": "PayPal, Klarna, carte bancaire (Visa & Mastercard) et virement bancaire (paiement anticipé)." },
|
||||
{ "q": "Quels moyens de paiement sont acceptés ?", "a": "PayPal (aussi en tant qu'invité par carte bancaire, sans compte PayPal) et virement bancaire (paiement anticipé)." },
|
||||
{ "q": "Puis-je retourner ma commande ?", "a": "Le droit de rétractation légal de 14 jours s'applique (voir notre notice de rétractation). Pour les pièces personnalisées sur mesure, le droit de rétractation peut être exclu par la loi ; pour les articles standards de notre gamme, il s'applique normalement." },
|
||||
{ "q": "Dois-je payer la TVA ?", "a": "Non. Van's DIY & Bastelbedarf est une petite entreprise selon l'§19 UStG – aucune TVA n'est calculée ni indiquée." },
|
||||
{ "q": "Ai-je besoin d'un compte client pour commander ?", "a": "Non, vous pouvez aussi commander confortablement en tant qu'invité. Un compte est toutefois utile pour regrouper l'historique des commandes, la liste de souhaits et le suivi de livraison." }
|
||||
|
||||
+24
-32
@@ -151,7 +151,7 @@ export const ui = {
|
||||
"Versandkostenfrei ab 75 € Warenwert (Deutschland), 85 € (Österreich, Luxemburg) bzw. 100 € (Schweiz)", "Der Versand erfolgt erst nach Zahlungseingang",
|
||||
"Bei Lieferungen in die Schweiz können Schweizer Einfuhrumsatzsteuer und Zollgebühren anfallen, die von dir als Empfänger:in getragen werden",
|
||||
],
|
||||
paymentTitle: "💳 Zahlungsarten", paymentItems: ["PayPal", "Klarna", "Kreditkarte (Visa & Mastercard)", "Banküberweisung (Vorkasse)"],
|
||||
paymentTitle: "💳 Zahlungsarten", paymentItems: ["PayPal", "Kredit-/Debitkarte als Gast über PayPal (Visa & Mastercard)", "Banküberweisung (Vorkasse)"],
|
||||
todoNote: "Konkrete Versandkosten-Staffel je Land/Warenwert wird ergänzt, sobald VanVan die finalen DHL-Konditionen festgelegt hat.",
|
||||
},
|
||||
cart: {
|
||||
@@ -177,7 +177,7 @@ export const ui = {
|
||||
},
|
||||
checkout: {
|
||||
eyebrow: "Checkout", title: "Bestellung abschließen",
|
||||
todoNote: "Phase 1 – Demo-Checkout. Es findet noch keine echte Zahlung statt. Die vollständige Anbindung an PayPal, Klarna und Banküberweisung sowie die Bestellverwaltung folgen in Phase 2 (Cloudflare Worker + D1, siehe Tech-Stack-Entscheidung im Projekt-Vault).",
|
||||
todoNote: "PayPal ist technisch fertig angebunden (echte, server-seitig geprüfte Zahlung) — bis ein echtes PayPal-Business-Konto hinterlegt ist, läuft es im kostenlosen PayPal-Testmodus, ohne dass echtes Geld fließt. Banküberweisung ist zu 100 % echt.",
|
||||
step1: "2. Kontakt & Lieferadresse", email: "E-Mail", firstName: "Vorname", lastName: "Nachname",
|
||||
street: "Straße & Hausnummer", zip: "PLZ", city: "Ort", country: "Land",
|
||||
step2: "1. Zahlungsart",
|
||||
@@ -185,12 +185,10 @@ export const ui = {
|
||||
revocationPrefix: "Ich habe die", revocationLink: "Widerrufsbelehrung", revocationAnd: "und",
|
||||
privacyLink: "Datenschutzerklärung", revocationSuffix: "zur Kenntnis genommen.",
|
||||
bankTransfer: "Banküberweisung (Vorkasse)", orderButton: "Jetzt zahlungspflichtig bestellen", emptyCartAlert: "Dein Warenkorb ist leer.",
|
||||
creditCard: "Kreditkarte", creditCardSub: "Visa & Mastercard",
|
||||
payPalHint: "Bezahle sicher und schnell direkt mit deinem PayPal-Konto.",
|
||||
klarnaHint: "Rechnung, Ratenkauf oder Sofortüberweisung – wähle bei Klarna, wie du zahlen möchtest.",
|
||||
klarnaConnect: "Weiter zu Klarna →", klarnaConnecting: "Verbinde mit Klarna …", klarnaConnected: "Verbindung zu Klarna hergestellt (Demo)",
|
||||
cardHint: "Deine Kartendaten werden sicher übertragen.", cardNumber: "Kartennummer", cardExpiry: "Gültig bis", cardCvc: "CVC",
|
||||
bankHintReal: "Bitte überweise den Rechnungsbetrag nach Bestellabschluss an folgendes Konto. Deine Ware wird nach Zahlungseingang verschickt.",
|
||||
payPalSub: "auch mit Kreditkarte, ohne eigenes Konto",
|
||||
payPalHint: "Bezahle sicher direkt mit deinem PayPal-Konto oder als Gast per Kredit-/Debitkarte – deine Kartendaten laufen dabei ausschließlich über PayPals eigene, gesicherte Seite, nie über unsere.",
|
||||
payPalCompleteHint: "Schließe deine Bestellung oben über den PayPal-Button ab – dort bezahlst du direkt, danach ist deine Bestellung sofort da.",
|
||||
bankHintReal: "Bitte überweise den Rechnungsbetrag nach Bestellabschluss an folgendes Konto. Deine Ware wird erst nach Zahlungseingang verschickt.",
|
||||
bankPlaceholder: "Die Bankverbindung wird dir nach Bestelleingang per E-Mail zugesendet.",
|
||||
bankHolder: "Kontoinhaber:in", bankIban: "IBAN", bankBic: "BIC", bankBankName: "Bank",
|
||||
shippingHintTitle: "Versandhinweis",
|
||||
@@ -455,7 +453,7 @@ export const ui = {
|
||||
"Free shipping from €75 order value (Germany), €85 (Austria, Luxembourg) or €100 (Switzerland)", "Shipping only starts after payment has been received",
|
||||
"Deliveries to Switzerland may be subject to Swiss import VAT and customs fees, payable by you as the recipient",
|
||||
],
|
||||
paymentTitle: "💳 Payment methods", paymentItems: ["PayPal", "Klarna", "Credit card (Visa & Mastercard)", "Bank transfer (prepayment)"],
|
||||
paymentTitle: "💳 Payment methods", paymentItems: ["PayPal", "Credit/debit card as a guest via PayPal (Visa & Mastercard)", "Bank transfer (prepayment)"],
|
||||
todoNote: "Concrete shipping-cost tiers per country/order value will be added once VanVan finalizes the DHL terms.",
|
||||
},
|
||||
cart: {
|
||||
@@ -481,7 +479,7 @@ export const ui = {
|
||||
},
|
||||
checkout: {
|
||||
eyebrow: "Checkout", title: "Complete your order",
|
||||
todoNote: "Phase 1 – demo checkout. No real payment is processed yet. Full integration with PayPal, Klarna and bank transfer, plus order management, follow in Phase 2 (Cloudflare Worker + D1, see the tech-stack decision in the project vault).",
|
||||
todoNote: "PayPal is technically fully connected (real, server-verified payment) — until a real PayPal Business account is set up, it runs in PayPal's free test mode, so no real money moves yet. Bank transfer is 100% real.",
|
||||
step1: "2. Contact & shipping address", email: "Email", firstName: "First name", lastName: "Last name",
|
||||
street: "Street & house number", zip: "ZIP code", city: "City", country: "Country",
|
||||
step2: "1. Payment method",
|
||||
@@ -489,12 +487,10 @@ export const ui = {
|
||||
revocationPrefix: "I have taken note of the", revocationLink: "withdrawal notice", revocationAnd: "and",
|
||||
privacyLink: "privacy policy", revocationSuffix: ".",
|
||||
bankTransfer: "Bank transfer (prepayment)", orderButton: "Order now, subject to payment", emptyCartAlert: "Your cart is empty.",
|
||||
creditCard: "Credit card", creditCardSub: "Visa & Mastercard",
|
||||
payPalHint: "Pay securely and instantly with your PayPal account.",
|
||||
klarnaHint: "Invoice, installments or instant transfer – choose how you'd like to pay with Klarna.",
|
||||
klarnaConnect: "Continue to Klarna →", klarnaConnecting: "Connecting to Klarna …", klarnaConnected: "Connected to Klarna (demo)",
|
||||
cardHint: "Your card details are transmitted securely.", cardNumber: "Card number", cardExpiry: "Expiry", cardCvc: "CVC",
|
||||
bankHintReal: "Please transfer the invoice amount to the following account after completing your order. Your items ship once payment is received.",
|
||||
payPalSub: "also with credit card, no account needed",
|
||||
payPalHint: "Pay securely with your PayPal account or as a guest by credit/debit card – your card details are handled entirely on PayPal's own secure page, never on ours.",
|
||||
payPalCompleteHint: "Complete your order via the PayPal button above – you pay there directly, and your order is placed right after.",
|
||||
bankHintReal: "Please transfer the invoice amount to the following account after completing your order. Your items ship only once payment is received.",
|
||||
bankPlaceholder: "Bank details will be sent to you by email after your order.",
|
||||
bankHolder: "Account holder", bankIban: "IBAN", bankBic: "BIC", bankBankName: "Bank",
|
||||
shippingHintTitle: "Shipping note",
|
||||
@@ -759,7 +755,7 @@ export const ui = {
|
||||
"Gratisversand ab 75 € Warewärt (Dütschland), 85 € (Öschterrych, Luxemburg) bzw. 100 € (Schwiz)", "Dr Versand geit erscht los, wenn d'Zahlig igange isch",
|
||||
"Bi Liefrige i d'Schwiz chöi schwyzerischi Iifuehrumsatzsteuer und Zollgebühre afalle, wo du als Empfänger:in trage muesch",
|
||||
],
|
||||
paymentTitle: "💳 Zahligsarte", paymentItems: ["PayPal", "Klarna", "Kreditkarte (Visa & Mastercard)", "Banküberwysig (Vorusszahlig)"],
|
||||
paymentTitle: "💳 Zahligsarte", paymentItems: ["PayPal", "Kredit-/Debitkarte als Gast über PayPal (Visa & Mastercard)", "Banküberwysig (Vorusszahlig)"],
|
||||
todoNote: "Di gnaui Versandchoschte-Staffle je Land/Warewärt wird ergänzt, sobald VanVan di definitive DHL-Konditione festgleit hät.",
|
||||
},
|
||||
cart: {
|
||||
@@ -785,7 +781,7 @@ export const ui = {
|
||||
},
|
||||
checkout: {
|
||||
eyebrow: "Checkout", title: "Bschtellig abschliesse",
|
||||
todoNote: "Phase 1 – Demo-Checkout. Es git no kei echti Zahlig. D'vollständigi Aabindig a PayPal, Klarna und Banküberwysig sowie d'Bstellverwaltig chöme i Phase 2.",
|
||||
todoNote: "PayPal isch technisch fertig aabunde (echti, server-seitig gprüefti Zahlig) — bis es echts PayPal-Business-Konto hinterlegt isch, laufts im gratis PayPal-Testmodus, ohni dass echts Gäld flüsst. Banküberwysig isch zu 100 % echt.",
|
||||
step1: "2. Kontakt & Lieferadrässe", email: "E-Mail", firstName: "Vorname", lastName: "Nachname",
|
||||
street: "Strasse & Huusnummere", zip: "PLZ", city: "Ort", country: "Land",
|
||||
step2: "1. Zahligsart",
|
||||
@@ -793,12 +789,10 @@ export const ui = {
|
||||
revocationPrefix: "Ich ha d'", revocationLink: "Widerrufsbelehrig", revocationAnd: "und d'",
|
||||
privacyLink: "Datenschutzerklärig", revocationSuffix: "zur Kenntnis gnoh.",
|
||||
bankTransfer: "Banküberwysig (Vorusszahlig)", orderButton: "Jetzt zahligspflichtig bstelle", emptyCartAlert: "Din Warenchorb isch leer.",
|
||||
creditCard: "Kreditkarte", creditCardSub: "Visa & Mastercard",
|
||||
payPalHint: "Zahl sicher und schnäll dirräkt mit dim PayPal-Konto.",
|
||||
klarnaHint: "Rächnig, Ratechauf oder Sofortiübrwysig – wähl bi Klarna, wie du zahle wosch.",
|
||||
klarnaConnect: "Wyter zu Klarna →", klarnaConnecting: "Verbindet mit Klarna …", klarnaConnected: "Verbindig zu Klarna hergstellt (Demo)",
|
||||
cardHint: "Dini Kartedate werde sicher übertreit.", cardNumber: "Kartenummere", cardExpiry: "Gültig bis", cardCvc: "CVC",
|
||||
bankHintReal: "Bitte überwys dr Rächnigsbetrag nach Bstellabschluss uf das Konto. Dini Ware wird nach Zahligsigang verschickt.",
|
||||
payPalSub: "au mit Kreditkarte, ohni eigenes Konto",
|
||||
payPalHint: "Zahl sicher dirräkt mit dim PayPal-Konto oder als Gast mit Kredit-/Debitkarte – dini Kartedate laufe dabi nur über PayPals eigeni, gsicherti Site, nie über üsi.",
|
||||
payPalCompleteHint: "Schliess dini Bstellig obe über de PayPal-Knopf ab – dört zahlsch dirräkt, danach isch dini Bstellig sofort da.",
|
||||
bankHintReal: "Bitte überwys dr Rächnigsbetrag nach Bstellabschluss uf das Konto. Dini Ware wird erst nach Zahligsigang verschickt.",
|
||||
bankPlaceholder: "D'Bankverbindig wird dir nach Bstelligang per E-Mail zuegschickt.",
|
||||
bankHolder: "Kontoinhaber:in", bankIban: "IBAN", bankBic: "BIC", bankBankName: "Bank",
|
||||
shippingHintTitle: "Versandhinwys",
|
||||
@@ -1063,7 +1057,7 @@ export const ui = {
|
||||
"Livraison gratuite dès 75 € d'achat (Allemagne), 85 € (Autriche, Luxembourg) ou 100 € (Suisse)", "L'expédition n'a lieu qu'après réception du paiement",
|
||||
"Pour les livraisons en Suisse, la TVA à l'importation suisse et des frais de douane peuvent s'appliquer, à la charge du/de la destinataire",
|
||||
],
|
||||
paymentTitle: "💳 Moyens de paiement", paymentItems: ["PayPal", "Klarna", "Carte bancaire (Visa & Mastercard)", "Virement bancaire (paiement anticipé)"],
|
||||
paymentTitle: "💳 Moyens de paiement", paymentItems: ["PayPal", "Carte bancaire en tant qu'invité via PayPal (Visa & Mastercard)", "Virement bancaire (paiement anticipé)"],
|
||||
todoNote: "Le barème précis des frais de port par pays/valeur de commande sera ajouté dès que VanVan aura fixé les conditions DHL définitives.",
|
||||
},
|
||||
cart: {
|
||||
@@ -1089,7 +1083,7 @@ export const ui = {
|
||||
},
|
||||
checkout: {
|
||||
eyebrow: "Paiement", title: "Finaliser la commande",
|
||||
todoNote: "Phase 1 – paiement de démonstration. Aucun paiement réel n'est encore traité. L'intégration complète avec PayPal, Klarna et le virement bancaire, ainsi que la gestion des commandes, suivront en phase 2.",
|
||||
todoNote: "PayPal est techniquement entièrement connecté (paiement réel, vérifié côté serveur) — tant qu'aucun compte PayPal Business réel n'est configuré, il fonctionne en mode test gratuit de PayPal, sans transfert d'argent réel. Le virement bancaire est 100 % réel.",
|
||||
step1: "2. Contact & adresse de livraison", email: "E-mail", firstName: "Prénom", lastName: "Nom",
|
||||
street: "Rue & numéro", zip: "Code postal", city: "Ville", country: "Pays",
|
||||
step2: "1. Moyen de paiement",
|
||||
@@ -1097,12 +1091,10 @@ export const ui = {
|
||||
revocationPrefix: "J'ai pris connaissance de la", revocationLink: "notice de rétractation", revocationAnd: "et de la",
|
||||
privacyLink: "politique de confidentialité", revocationSuffix: ".",
|
||||
bankTransfer: "Virement bancaire (paiement anticipé)", orderButton: "Commander avec obligation de paiement", emptyCartAlert: "Votre panier est vide.",
|
||||
creditCard: "Carte bancaire", creditCardSub: "Visa & Mastercard",
|
||||
payPalHint: "Payez en toute sécurité et instantanément avec votre compte PayPal.",
|
||||
klarnaHint: "Facture, paiement en plusieurs fois ou virement instantané – choisissez comment payer avec Klarna.",
|
||||
klarnaConnect: "Continuer vers Klarna →", klarnaConnecting: "Connexion à Klarna …", klarnaConnected: "Connecté à Klarna (démo)",
|
||||
cardHint: "Vos données de carte sont transmises en toute sécurité.", cardNumber: "Numéro de carte", cardExpiry: "Date d'expiration", cardCvc: "CVC",
|
||||
bankHintReal: "Veuillez virer le montant de la facture sur le compte suivant après avoir finalisé votre commande. Vos articles sont expédiés dès réception du paiement.",
|
||||
payPalSub: "aussi par carte bancaire, sans compte",
|
||||
payPalHint: "Payez en toute sécurité avec votre compte PayPal ou en tant qu'invité par carte bancaire – vos données de carte transitent uniquement par le site sécurisé de PayPal, jamais par le nôtre.",
|
||||
payPalCompleteHint: "Finalisez votre commande via le bouton PayPal ci-dessus – vous y payez directement, votre commande est passée juste après.",
|
||||
bankHintReal: "Veuillez virer le montant de la facture sur le compte suivant après avoir finalisé votre commande. Vos articles ne sont expédiés qu'après réception du paiement.",
|
||||
bankPlaceholder: "Les coordonnées bancaires vous seront envoyées par e-mail après votre commande.",
|
||||
bankHolder: "Titulaire du compte", bankIban: "IBAN", bankBic: "BIC", bankBankName: "Banque",
|
||||
shippingHintTitle: "Remarque sur la livraison",
|
||||
|
||||
+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) {
|
||||
|
||||
+30
-4
@@ -1709,6 +1709,7 @@ a:focus-visible, button:focus-visible {
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .order-card, .order-card::after { animation: none; } }
|
||||
|
||||
.order-card-zahlungOffen { --status-color: #e08a3c; }
|
||||
.order-card-bezahlt { --status-color: #9b7fd6; }
|
||||
.order-card-bearbeitung { --status-color: #e3b23c; }
|
||||
.order-card-versandVorbereitet { --status-color: #e3b23c; }
|
||||
@@ -1718,6 +1719,7 @@ a:focus-visible, button:focus-visible {
|
||||
|
||||
.order-status { display: inline-flex; align-items: center; gap: 0.4em; }
|
||||
.order-status::before { font-size: 0.95em; }
|
||||
.order-status-zahlungOffen::before { content: "⏳"; }
|
||||
.order-status-bezahlt::before { content: "💳"; }
|
||||
.order-status-bearbeitung::before { content: "⚙️"; }
|
||||
.order-status-versandVorbereitet::before { content: "📦"; }
|
||||
@@ -1725,6 +1727,7 @@ a:focus-visible, button:focus-visible {
|
||||
.order-status-abgeschlossen::before { content: "✅"; }
|
||||
.order-status-storniert::before { content: "❌"; }
|
||||
|
||||
.order-status-zahlungOffen { background: color-mix(in srgb, #e08a3c 30%, transparent); border-color: color-mix(in srgb, #e08a3c 55%, transparent); color: #ffd9ae; }
|
||||
.order-status-bezahlt { background: color-mix(in srgb, #9b7fd6 32%, transparent); border-color: color-mix(in srgb, #9b7fd6 55%, transparent); color: #d9cdf5; }
|
||||
.order-status-bearbeitung { background: color-mix(in srgb, #e3b23c 30%, transparent); border-color: color-mix(in srgb, #e3b23c 55%, transparent); color: #f6dda0; }
|
||||
.order-status-versandVorbereitet { background: color-mix(in srgb, #e3b23c 30%, transparent); border-color: color-mix(in srgb, #e3b23c 55%, transparent); color: #f6dda0; }
|
||||
@@ -2368,10 +2371,33 @@ a:focus-visible, button:focus-visible {
|
||||
.pay-connect-btn[disabled] { opacity: 0.75; cursor: default; }
|
||||
.pay-connect-status { margin: 0.7em 0 0; font-size: 0.88rem; color: var(--c-accent); font-weight: 600; }
|
||||
|
||||
/* Inline-Kartenformular (Kreditkarte) */
|
||||
.card-fields { display: grid; gap: 0.7rem; }
|
||||
.card-fields label { display: block; font-size: 0.8rem; color: var(--c-text-muted); margin-bottom: 0.3rem; }
|
||||
.card-fields input { font-variant-numeric: tabular-nums; letter-spacing: 0.03em; }
|
||||
/* Kleine Visa/Mastercard-Hinweis-Badges direkt neben "PayPal" — zeigt, dass Kartenzahlung über
|
||||
den PayPal-Gast-Checkout ebenfalls möglich ist (Kartendaten laufen dabei komplett über PayPals
|
||||
eigene, PCI-DSS-zertifizierte Oberfläche, nie über unsere Seite — siehe Recherche zur
|
||||
Kreditkarten-Rechtslage: eine eigene Karten-Eingabemaske wäre PCI-DSS-pflichtig und für einen
|
||||
kleinen Shop praktisch nicht stemmbar). */
|
||||
.pay-sub-cards { display: inline-flex; align-items: center; gap: 0.3em; margin-left: 0.5em; vertical-align: middle; }
|
||||
.pay-sub-cards .mini-card { width: 26px; height: 18px; border-radius: 4px; }
|
||||
.pay-sub-cards .mini-card svg { width: 16px; height: auto; }
|
||||
|
||||
/* Der große "Jetzt zahlungspflichtig bestellen"-Knopf gehört NUR zur Überweisung (Vorkasse) —
|
||||
bei PayPal läuft die Zahlung ausschließlich über die echten PayPal-Smart-Buttons oben (siehe
|
||||
#paypal-button-container), die selbst die Bestellung auslösen, sobald die Zahlung bei PayPal
|
||||
bestätigt ist. Der Knopf würde bei ausgewähltem PayPal sonst fälschlich eine "Bestellung ohne
|
||||
Zahlung" ermöglichen — deshalb hier komplett ausgeblendet, kein Fallback, keine Ausnahme. */
|
||||
#checkout-form:has(input[name="pay"][value="paypal"]:checked) #order-submit-btn { display: none; }
|
||||
#checkout-form:has(input[name="pay"][value="paypal"]:checked) #order-submit-hint { display: block; }
|
||||
#order-submit-hint {
|
||||
display: none;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.9em 1.1em;
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid color-mix(in srgb, var(--c-accent) 35%, transparent);
|
||||
background: color-mix(in srgb, var(--c-accent) 8%, transparent);
|
||||
font-size: 0.88rem;
|
||||
color: var(--c-text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Bankverbindung (Vorkasse) — echte, direkt aus dem Adminbereich gespeiste Kontodaten. */
|
||||
.bank-details {
|
||||
|
||||
Reference in New Issue
Block a user