diff options
| author | Roger Rehnelt <noreply@maennchen1.de> | 2026-10-07 14:29:26 +0200 |
|---|---|---|
| committer | Roger Rehnelt <noreply@maennchen1.de> | 2026-10-07 14:29:26 +0200 |
| commit | 4baa7c13be1b538b8c8bf1df18b39ddea51307fd (patch) | |
| tree | f77652248318a321398eccf15d6cebd913b458e6 /views | |
| parent | 41905a4a9386326bbbede4fbfd1d7b03b5c4d16b (diff) | |
| parent | 52df56d7476408d2c2ac5b729b6e02202524a0f3 (diff) | |
Merge main nach fix/4.6.1: SmartCheckout „Bestellabschluss“ von Daniel
Daniels Commit 52df56d7 lag nur auf main. Changelog in der kurzen Form, Übersetzungen beider Seiten zusammengeführt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'views')
| -rw-r--r-- | views/css/smartcheckout.css | 11 | ||||
| -rw-r--r-- | views/js/smartcheckout.js | 115 | ||||
| -rw-r--r-- | views/mods/mod_smartcheckout/checkout.phtml | 441 | ||||
| -rw-r--r-- | views/mods/mod_smartcheckout/settings_edit.phtml | 13 |
4 files changed, 372 insertions, 208 deletions
diff --git a/views/css/smartcheckout.css b/views/css/smartcheckout.css index b751f70..c044545 100644 --- a/views/css/smartcheckout.css +++ b/views/css/smartcheckout.css @@ -43,6 +43,11 @@ html body.content-style-boxed .entry-content .wpsg_smartcheckout_after.alignwide .wpsg_smartcheckout .sc_layout { display:grid; grid-template-columns:minmax(0, 1fr) calc(21 * var(--sc-u, 1rem)); gap:var(--sc-gap); align-items:start; } .wpsg_smartcheckout_single .sc_layout { grid-template-columns:minmax(0, 1fr); } .wpsg_smartcheckout .sc_main { display:flex; flex-direction:column; min-width:0; } +/* Hülle der Eingaben, im Bestätigungsschritt gesperrt; ohne die Rahmen und Abstände, die Browser einem fieldset geben */ +.wpsg_smartcheckout .sc_fieldset { display:flex; flex-direction:column; min-width:0; margin:0; padding:0; border:0; } +.wpsg_smartcheckout .sc_fieldset:disabled .sc_box { opacity:0.75; } +.wpsg_smartcheckout .sc_confirm_note { margin-bottom:var(--sc-gap); padding:calc(0.9 * var(--sc-u, 1rem)); border-left:3px solid var(--sc-accent); background:var(--sc-flat); scroll-margin-top:calc(1.5 * var(--sc-u, 1rem)); } +.wpsg_smartcheckout .sc_confirm_note p { margin:calc(0.4 * var(--sc-u, 1rem)) 0 calc(0.75 * var(--sc-u, 1rem)) 0; } .wpsg_smartcheckout .sc_side { position:sticky; top:calc(1.5 * var(--sc-u, 1rem)); } .wpsg_smartcheckout_single .sc_side { position:static; } .wpsg_smartcheckout .sc_layout.sc_busy { opacity:0.6; pointer-events:none; transition:opacity 0.15s ease-out; } @@ -259,6 +264,11 @@ html body.content-style-boxed .entry-content .wpsg_smartcheckout_after.alignwide /* Zusammenfassung */ .wpsg_smartcheckout .sc_summary { position:relative; padding:calc(1.25 * var(--sc-u, 1rem)); border:1px solid var(--sc-line-soft); border-radius:var(--sc-radius); } .wpsg_smartcheckout_single .sc_side { margin-top:var(--sc-gap); } +.wpsg_smartcheckout .sc_sum_products { display:flex; flex-direction:column; gap:calc(0.3 * var(--sc-u, 1rem)); margin-bottom:calc(0.75 * var(--sc-u, 1rem)); padding-bottom:calc(0.75 * var(--sc-u, 1rem)); border-bottom:1px solid var(--sc-line-soft); font-size:calc(0.85 * var(--sc-u, 1rem)); } +.wpsg_smartcheckout .sc_sum_products:empty { display:none; } +.wpsg_smartcheckout .sc_sum_product { display:flex; justify-content:space-between; gap:var(--sc-u, 1rem); align-items:baseline; } +.wpsg_smartcheckout .sc_sum_product_name { min-width:0; overflow-wrap:anywhere; } +.wpsg_smartcheckout .sc_sum_product_price { white-space:nowrap; } .wpsg_smartcheckout .sc_sum_rows { display:flex; flex-direction:column; gap:calc(0.4 * var(--sc-u, 1rem)); } .wpsg_smartcheckout .sc_sum_row { display:flex; justify-content:space-between; gap:var(--sc-u, 1rem); align-items:baseline; font-size:calc(0.95 * var(--sc-u, 1rem)); } .wpsg_smartcheckout .sc_sum_value { white-space:nowrap; } @@ -301,6 +311,7 @@ html body.content-style-boxed .entry-content .wpsg_smartcheckout_after.alignwide .wpsg_smartcheckout .sc_button:disabled { opacity:0.5; cursor:default; } .wpsg_smartcheckout .sc_button_secondary { padding:calc(0.5 * var(--sc-u, 1rem)) calc(0.9 * var(--sc-u, 1rem)); border-color:var(--sc-line); font-size:calc(0.9 * var(--sc-u, 1rem)); } .wpsg_smartcheckout .sc_button_order { width:100%; margin-top:calc(1.1 * var(--sc-u, 1rem)); font-weight:600; } +.wpsg_smartcheckout .sc_button_edit { width:100%; margin-top:calc(0.5 * var(--sc-u, 1rem)); } /* Ladeanzeige */ .wpsg_smartcheckout .sc_spinner { position:absolute; top:calc(1.25 * var(--sc-u, 1rem)); right:calc(1.25 * var(--sc-u, 1rem)); } diff --git a/views/js/smartcheckout.js b/views/js/smartcheckout.js index 20674f8..9c4f4f1 100644 --- a/views/js/smartcheckout.js +++ b/views/js/smartcheckout.js @@ -46,11 +46,25 @@ queued: false, queuedUpload: null, submitting: false, + confirm: false, timer: null }; }, + computed: { + + /** + * Sind Bestellbedingungen und Bestellbutton zu sehen? Beim Bestellabschluss „Zusammenfassung vor Abschluss“ erst im Bestätigungsschritt + */ + showFinal: function() { + + return (this.state.finish !== 'confirm' || this.confirm); + + } // showFinal() + + }, + created: function() { this.syncForm(true); @@ -65,8 +79,8 @@ // Nach einem abgelehnten Bestellversuch steht der Kunde vor dem ersten Fehler if (this.state.errors.length !== 0) this.$nextTick(this.focusError); - // Das GARAN-Gewährleistungslabel erscheint wie in der klassischen Zusammenfassung beim Betreten der Seite - if (this.state.warranty && this.state.warranty.popup === true && this.state.empty !== true) this.garanPopup = true; + // Das GARAN-Gewährleistungslabel erscheint wie in der klassischen Zusammenfassung beim Betreten der Seite, mit Bestätigungsschritt erst dort + if (this.state.warranty && this.state.warranty.popup === true && this.state.empty !== true && this.state.finish !== 'confirm') this.garanPopup = true; // Die Lightbox des Gewährleistungslabels lässt sich mit Esc schließen var self = this; @@ -77,10 +91,13 @@ }); - // Zurück aus dem Seiten-Cache des Browsers (z.B. Zurück-Button nach dem Zahlungsanbieter): Layer wieder entfernen + // Zurück aus dem Seiten-Cache des Browsers (z.B. Zurück-Button nach dem Zahlungsanbieter): Layer entfernen, Eingaben wieder bearbeitbar window.addEventListener('pageshow', function(event) { - if (event.persisted) self.submitting = false; + if (!event.persisted) return; + + self.submitting = false; + self.confirm = false; }); @@ -157,7 +174,7 @@ selectChoice: function(type, id, event) { if (event && event.target && typeof event.target.closest === 'function' && event.target.closest('.sc_choice_head') !== null) return; - if (this.form[type] === id) return; + if (this.form[type] === id || this.confirm) return; this.form[type] = id; this.refresh(); @@ -242,7 +259,8 @@ var el_form = document.getElementById('wpsg_smartcheckout_form'); - if (el_form === null) return; + // Im Bestätigungsschritt sind die Eingaben gesperrt und würden ohne sie übertragen + if (el_form === null || this.confirm) return; // Dateien der direkt hochzuladenden Felder sofort sichern: der Block wird mit der nächsten Antwort neu aufgebaut var arUpload = []; @@ -358,6 +376,8 @@ } + if (action === 'confirm') self.afterConfirm(result.confirm === true); + if (self.queuedUpload !== null) { var upload = self.queuedUpload; @@ -477,8 +497,75 @@ }, // focusError() /** - * Blockt das Absenden, solange der Server noch rechnet + * Prüft die Angaben und wechselt in den Bestätigungsschritt (Bestellabschluss „Zusammenfassung vor Abschluss“) + * + * Die Pflichtfelder der Eingaben prüft zuerst der Browser, Bestellbedingungen und AGB erst der Bestätigungsschritt. */ + startConfirm: function() { + + if (this.busy) return; + + var el_fieldset = el_root.querySelector('.sc_fieldset'); + + if (el_fieldset !== null && typeof el_fieldset.reportValidity === 'function' && !el_fieldset.reportValidity()) return; + + // Eine noch ausstehende Änderung geht in derselben Anfrage mit + if (this.timer !== null) { + + window.clearTimeout(this.timer); + this.timer = null; + + } + + this.send('confirm'); + + }, // startConfirm() + + /** + * Antwort auf startConfirm(): bei bestandener Prüfung Eingaben sperren, sonst zum ersten Fehler + */ + afterConfirm: function(bConfirm) { + + var self = this; + + if (!bConfirm) { + + this.$nextTick(this.focusError); + + return; + + } + + this.confirm = true; + + // Das GARAN-Gewährleistungslabel erscheint wie in der klassischen Zusammenfassung + if (this.state.warranty && this.state.warranty.popup === true && this.garanConfirmed !== true) this.garanPopup = true; + + this.$nextTick(function() { + + var el_note = el_root.querySelector('.sc_confirm_note'); + + if (el_note !== null) el_note.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + }); + + }, // afterConfirm(bConfirm) + + /** + * Verlässt den Bestätigungsschritt, die Eingaben sind wieder bearbeitbar + */ + editConfirm: function() { + + this.confirm = false; + + this.$nextTick(function() { + + el_root.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + }); + + }, // editConfirm() + /** * Berechnet neu, wenn sich eine Eingabe in einem Block der Module ändert (z.B. Lieferadresse, Produktvariablen) * @@ -698,6 +785,20 @@ } + /* + * Gesperrte Felder würden nicht übertragen. Der Browser stellt die Formulardaten erst nach diesem Ereignis + * zusammen, deshalb hier direkt im DOM freigeben und nicht über Vue (das aktualisiert erst später). + */ + if (this.confirm) { + + el_root.querySelectorAll('fieldset.sc_fieldset').forEach(function(el) { + + el.disabled = false; + + }); + + } + // Ab hier wird tatsächlich bestellt: Layer über die Seite, bis der Browser die nächste Seite lädt this.submitting = true; diff --git a/views/mods/mod_smartcheckout/checkout.phtml b/views/mods/mod_smartcheckout/checkout.phtml index 947f3d8..043b151 100644 --- a/views/mods/mod_smartcheckout/checkout.phtml +++ b/views/mods/mod_smartcheckout/checkout.phtml @@ -46,269 +46,281 @@ <div class="sc_main"> - <?php /* Schritt 1: Warenkorb */ ?> - <section class="sc_box" aria-labelledby="sc_title_basket"> - <h2 class="sc_title" id="sc_title_basket"><span class="sc_step">1</span><?php echo esc_html__('Ihr Warenkorb', 'wpsg'); ?></h2> - - <div class="sc_modul_basket sc_modul_dynamic" v-if="state.html.basketTop" v-html="state.html.basketTop"></div> + <?php /* Bestätigungsschritt beim Bestellabschluss „Zusammenfassung vor Abschluss“ */ ?> + <div class="sc_confirm_note" v-if="confirm" role="status"> + <strong><?php echo esc_html__('Bitte prüfen Sie Ihre Angaben', 'wpsg'); ?></strong> + <p><?php echo esc_html__('Alle Eingaben sind jetzt gesperrt. Stimmt alles, schließen Sie die Bestellung in der Zusammenfassung ab. Um etwas zu korrigieren, klicken Sie auf „Angaben ändern“.', 'wpsg'); ?></p> + <button type="button" class="sc_button sc_button_secondary" @click="editConfirm" :disabled="busy"><?php echo esc_html__('Angaben ändern', 'wpsg'); ?></button> + </div> - <div class="sc_products"> + <?php /* Im Bestätigungsschritt sind alle Eingaben gesperrt, vor dem Absenden gibt smartcheckout.js sie wieder frei */ ?> + <fieldset class="sc_fieldset" :disabled="confirm"> - <div class="sc_product" v-for="product in state.products" :key="product.index"> + <?php /* Schritt 1: Warenkorb */ ?> + <section class="sc_box" aria-labelledby="sc_title_basket"> + <h2 class="sc_title" id="sc_title_basket"><span class="sc_step">1</span><?php echo esc_html__('Ihr Warenkorb', 'wpsg'); ?></h2> - <div class="sc_product_image" v-if="product.image"> - <img :src="product.image" :alt="product.name" loading="lazy" /> - </div> + <div class="sc_modul_basket sc_modul_dynamic" v-if="state.html.basketTop" v-html="state.html.basketTop"></div> - <div class="sc_product_body"> + <div class="sc_products"> - <?php /* Angaben der Module vor dem Produkt, z.B. Maße beim Druckshop */ ?> - <div class="sc_product_before sc_modul_dynamic" v-if="product.before" v-html="product.before"></div> + <div class="sc_product" v-for="product in state.products" :key="product.index"> - <div class="sc_product_name"> - <a v-if="product.link" :href="product.link">{{ product.name }}</a> - <span v-else>{{ product.name }}</span> + <div class="sc_product_image" v-if="product.image"> + <img :src="product.image" :alt="product.name" loading="lazy" /> </div> - <div class="sc_product_desc" v-if="product.shortdesc" v-html="product.shortdesc"></div> - <div class="sc_product_extra sc_modul_dynamic" v-if="product.extra" v-html="product.extra"></div> - <div class="sc_product_garan" v-if="product.garan" v-html="product.garan"></div> - <div class="sc_product_revocation" v-if="product.revocation" v-html="product.revocation"></div> + <div class="sc_product_body"> - <div class="sc_product_tax" v-if="state.showTax"> - <?php echo esc_html__('MwSt.', 'wpsg'); ?> {{ product.tax_value }} - </div> + <?php /* Angaben der Module vor dem Produkt, z.B. Maße beim Druckshop */ ?> + <div class="sc_product_before sc_modul_dynamic" v-if="product.before" v-html="product.before"></div> - </div> + <div class="sc_product_name"> + <a v-if="product.link" :href="product.link">{{ product.name }}</a> + <span v-else>{{ product.name }}</span> + </div> - <div class="sc_product_amount"> + <div class="sc_product_desc" v-if="product.shortdesc" v-html="product.shortdesc"></div> + <div class="sc_product_extra sc_modul_dynamic" v-if="product.extra" v-html="product.extra"></div> + <div class="sc_product_garan" v-if="product.garan" v-html="product.garan"></div> + <div class="sc_product_revocation" v-if="product.revocation" v-html="product.revocation"></div> + + <div class="sc_product_tax" v-if="state.showTax"> + <?php echo esc_html__('MwSt.', 'wpsg'); ?> {{ product.tax_value }} + </div> - <div class="sc_stepper" v-if="!product.fixed"> - <button type="button" class="sc_stepper_btn" @click="changeAmount(product, -1)" :disabled="busy" :aria-label="'<?php echo esc_js(__('Menge verringern', 'wpsg')); ?>'">−</button> - <input - type="text" - inputmode="numeric" - class="sc_stepper_input" - :name="'wpsg[menge][' + product.index + ']'" - v-model="amounts[product.index]" - @change="refresh" - :aria-label="'<?php echo esc_js(__('Menge', 'wpsg')); ?>'" - /> - <button type="button" class="sc_stepper_btn" @click="changeAmount(product, 1)" :disabled="busy" :aria-label="'<?php echo esc_js(__('Menge erhöhen', 'wpsg')); ?>'">+</button> </div> - <div class="sc_stepper sc_stepper_fixed" v-else> - <input type="hidden" :name="'wpsg[menge][' + product.index + ']'" :value="product.amount" /> - <span class="sc_stepper_static">{{ product.amount }}</span> + <div class="sc_product_amount"> + + <div class="sc_stepper" v-if="!product.fixed"> + <button type="button" class="sc_stepper_btn" @click="changeAmount(product, -1)" :disabled="busy" :aria-label="'<?php echo esc_js(__('Menge verringern', 'wpsg')); ?>'">−</button> + <input + type="text" + inputmode="numeric" + class="sc_stepper_input" + :name="'wpsg[menge][' + product.index + ']'" + v-model="amounts[product.index]" + @change="refresh" + :aria-label="'<?php echo esc_js(__('Menge', 'wpsg')); ?>'" + /> + <button type="button" class="sc_stepper_btn" @click="changeAmount(product, 1)" :disabled="busy" :aria-label="'<?php echo esc_js(__('Menge erhöhen', 'wpsg')); ?>'">+</button> + </div> + + <div class="sc_stepper sc_stepper_fixed" v-else> + <input type="hidden" :name="'wpsg[menge][' + product.index + ']'" :value="product.amount" /> + <span class="sc_stepper_static">{{ product.amount }}</span> + </div> + + <button type="button" class="sc_remove" @click="removeProduct(product)" :disabled="busy"> + <?php echo esc_html__('Entfernen', 'wpsg'); ?> + </button> + </div> - <button type="button" class="sc_remove" @click="removeProduct(product)" :disabled="busy"> - <?php echo esc_html__('Entfernen', 'wpsg'); ?> - </button> + <div class="sc_product_price"> + <div class="sc_product_price_total">{{ product.price_total }}</div> + <div class="sc_product_price_single" v-if="product.amount !== 1">{{ product.price }} <?php echo esc_html__('je Stück', 'wpsg'); ?></div> + </div> - </div> + <?php /* Eingabefelder der Module (z.B. Produktvariablen) über die volle Breite der Position */ ?> + <div class="sc_product_fields sc_modul_dynamic" v-if="product.fields" v-html="product.fields"></div> - <div class="sc_product_price"> - <div class="sc_product_price_total">{{ product.price_total }}</div> - <div class="sc_product_price_single" v-if="product.amount !== 1">{{ product.price }} <?php echo esc_html__('je Stück', 'wpsg'); ?></div> </div> - <?php /* Eingabefelder der Module (z.B. Produktvariablen) über die volle Breite der Position */ ?> - <div class="sc_product_fields sc_modul_dynamic" v-if="product.fields" v-html="product.fields"></div> - </div> - </div> + <div class="sc_modul_basket sc_modul_dynamic" v-if="state.html.basketAfterProducts" v-html="state.html.basketAfterProducts"></div> - <div class="sc_modul_basket sc_modul_dynamic" v-if="state.html.basketAfterProducts" v-html="state.html.basketAfterProducts"></div> - - <?php if ($this->hasMod('wpsg_mod_gutschein')) { ?> - <div class="sc_voucher"> - - <label class="sc_voucher_label" for="wpsg_smartcheckout_voucher"><?php echo esc_html__('Gutscheincode', 'wpsg'); ?></label> - - <div class="sc_voucher_row"> - <input - type="text" - id="wpsg_smartcheckout_voucher" - class="sc_voucher_input" - :class="{ 'sc_invalid': voucherError }" - name="wpsg[gutschein]" - v-model="voucher" - @keydown.enter.prevent="addVoucher" - /> - <button type="button" class="sc_button sc_button_secondary" @click="addVoucher" :disabled="busy || voucher === ''"> - <?php echo esc_html__('Einlösen', 'wpsg'); ?> - </button> - </div> + <?php if ($this->hasMod('wpsg_mod_gutschein')) { ?> + <div class="sc_voucher"> + + <label class="sc_voucher_label" for="wpsg_smartcheckout_voucher"><?php echo esc_html__('Gutscheincode', 'wpsg'); ?></label> - <div class="sc_voucher_list" v-if="state.hasVouchers"> - <div class="sc_voucher_item" v-for="item in state.vouchers" :key="item.id"> - <span class="sc_voucher_code"><template v-if="item.coupon"><?php echo esc_html__('Wertgutschein', 'wpsg'); ?> </template>{{ item.code }}</span> - <span class="sc_voucher_partial" v-if="item.partial"><?php echo esc_html__('teilweise angerechnet', 'wpsg'); ?></span> - <span class="sc_voucher_value">{{ item.value }}</span> - <button type="button" class="sc_remove" @click="removeVoucher(item)" :disabled="busy"><?php echo esc_html__('Entfernen', 'wpsg'); ?></button> + <div class="sc_voucher_row"> + <input + type="text" + id="wpsg_smartcheckout_voucher" + class="sc_voucher_input" + :class="{ 'sc_invalid': voucherError }" + name="wpsg[gutschein]" + v-model="voucher" + @keydown.enter.prevent="addVoucher" + /> + <button type="button" class="sc_button sc_button_secondary" @click="addVoucher" :disabled="busy || voucher === ''"> + <?php echo esc_html__('Einlösen', 'wpsg'); ?> + </button> </div> + + <div class="sc_voucher_list" v-if="state.hasVouchers"> + <div class="sc_voucher_item" v-for="item in state.vouchers" :key="item.id"> + <span class="sc_voucher_code"><template v-if="item.coupon"><?php echo esc_html__('Wertgutschein', 'wpsg'); ?> </template>{{ item.code }}</span> + <span class="sc_voucher_partial" v-if="item.partial"><?php echo esc_html__('teilweise angerechnet', 'wpsg'); ?></span> + <span class="sc_voucher_value">{{ item.value }}</span> + <button type="button" class="sc_remove" @click="removeVoucher(item)" :disabled="busy"><?php echo esc_html__('Entfernen', 'wpsg'); ?></button> + </div> + </div> + </div> + <?php } ?> - </div> - <?php } ?> + </section> - </section> + <?php /* Schritt 2: Kundendaten */ ?> + <section class="sc_box" aria-labelledby="sc_title_customer"> + <h2 class="sc_title" id="sc_title_customer"><span class="sc_step">2</span><?php echo esc_html__('Ihre Daten', 'wpsg'); ?></h2> - <?php /* Schritt 2: Kundendaten */ ?> - <section class="sc_box" aria-labelledby="sc_title_customer"> - <h2 class="sc_title" id="sc_title_customer"><span class="sc_step">2</span><?php echo esc_html__('Ihre Daten', 'wpsg'); ?></h2> + <?php /* Login und Registrierung der Kundenverwaltung, serverseitig gerendert */ ?> + <div class="sc_modul_login" v-pre> + <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_kundenverwaltung', 'checkout_login'); ?> + </div> - <?php /* Login und Registrierung der Kundenverwaltung, serverseitig gerendert */ ?> - <div class="sc_modul_login" v-pre> - <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_kundenverwaltung', 'checkout_login'); ?> - </div> + <div class="sc_fields"> - <div class="sc_fields"> - - <div - class="sc_field" - v-for="field in state.fields" - :key="field.name" - :class="['sc_field_' + field.width, { 'sc_invalid': hasError(field.name) }]" - > - - <label class="sc_label" :for="'wpsg_sc_' + field.name"> - {{ field.label }}<span class="sc_required" v-if="field.required" aria-hidden="true">*</span> - </label> - - <select - v-if="field.type === 'select'" - class="sc_input" - :id="'wpsg_sc_' + field.name" - :name="fieldName(field)" - :required="field.required" - v-model="form[field.name]" - @change="refresh" + <div + class="sc_field" + v-for="field in state.fields" + :key="field.name" + :class="['sc_field_' + field.width, { 'sc_invalid': hasError(field.name) }]" > - <option value="-1"><?php echo esc_html__('Bitte auswählen', 'wpsg'); ?></option> - <option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</option> - </select> - <template v-else-if="field.type === 'checkbox'"> - <input type="hidden" :name="fieldName(field)" value="0" /> + <label class="sc_label" :for="'wpsg_sc_' + field.name"> + {{ field.label }}<span class="sc_required" v-if="field.required" aria-hidden="true">*</span> + </label> + + <select + v-if="field.type === 'select'" + class="sc_input" + :id="'wpsg_sc_' + field.name" + :name="fieldName(field)" + :required="field.required" + v-model="form[field.name]" + @change="refresh" + > + <option value="-1"><?php echo esc_html__('Bitte auswählen', 'wpsg'); ?></option> + <option v-for="option in field.options" :key="option.value" :value="option.value">{{ option.label }}</option> + </select> + + <template v-else-if="field.type === 'checkbox'"> + <input type="hidden" :name="fieldName(field)" value="0" /> + <input + type="checkbox" + class="sc_checkbox" + :id="'wpsg_sc_' + field.name" + :name="fieldName(field)" + value="1" + :required="field.required" + v-model="form[field.name]" + true-value="1" + false-value="0" + @change="refresh" + /> + </template> + <input - type="checkbox" - class="sc_checkbox" + v-else + class="sc_input" + :type="field.type" :id="'wpsg_sc_' + field.name" :name="fieldName(field)" - value="1" :required="field.required" + :autocomplete="field.autocomplete || 'off'" v-model="form[field.name]" - true-value="1" - false-value="0" @change="refresh" + @paste="field.nopaste ? $event.preventDefault() : null" /> - </template> - - <input - v-else - class="sc_input" - :type="field.type" - :id="'wpsg_sc_' + field.name" - :name="fieldName(field)" - :required="field.required" - :autocomplete="field.autocomplete || 'off'" - v-model="form[field.name]" - @change="refresh" - @paste="field.nopaste ? $event.preventDefault() : null" - /> + + </div> </div> - </div> + <?php /* Erweiterungen der Module innerhalb der Kundendaten */ ?> + <div class="sc_modul_customer" v-pre> + <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_kundenverwaltung', 'checkout_customer_inner'); ?> + <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_nlsatolo', 'checkout_customer_inner'); ?> + <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_trustedshops', 'checkout_customer_inner'); ?> + <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_newsletter', 'checkout_customer_inner'); ?> + </div> - <?php /* Erweiterungen der Module innerhalb der Kundendaten */ ?> - <div class="sc_modul_customer" v-pre> - <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_kundenverwaltung', 'checkout_customer_inner'); ?> - <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_nlsatolo', 'checkout_customer_inner'); ?> - <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_trustedshops', 'checkout_customer_inner'); ?> - <?php echo \wpsgSmartcheckout\State::renderMod('wpsg_mod_newsletter', 'checkout_customer_inner'); ?> - </div> + <div class="sc_hint"> + <?php echo wpsg_translate(__('Mit #1# gekennzeichnete Felder sind Pflichtfelder.', 'wpsg'), '<span class="sc_required">*</span>'); ?> + </div> - <div class="sc_hint"> - <?php echo wpsg_translate(__('Mit #1# gekennzeichnete Felder sind Pflichtfelder.', 'wpsg'), '<span class="sc_required">*</span>'); ?> - </div> + </section> - </section> + <?php /* Abweichende Lieferanschrift und weitere Module vor dem Abschluss */ ?> + <?php $strPreButton = \wpsgSmartcheckout\State::renderHook('checkout_inner_prebutton', [&$this->view]); ?> + <?php if (trim($strPreButton) !== '') { ?> + <section class="sc_box sc_modul_extra" v-pre> + <?php echo $strPreButton; ?> + </section> + <?php } ?> - <?php /* Abweichende Lieferanschrift und weitere Module vor dem Abschluss */ ?> - <?php $strPreButton = \wpsgSmartcheckout\State::renderHook('checkout_inner_prebutton', [&$this->view]); ?> - <?php if (trim($strPreButton) !== '') { ?> - <section class="sc_box sc_modul_extra" v-pre> - <?php echo $strPreButton; ?> - </section> - <?php } ?> + <?php /* Blöcke der Module, die sich mit jeder Antwort ändern (z.B. Bestellupload) */ ?> + <section class="sc_box sc_modul_upload sc_modul_dynamic" v-if="state.html.checkoutDynamic" v-html="state.html.checkoutDynamic"></section> - <?php /* Blöcke der Module, die sich mit jeder Antwort ändern (z.B. Bestellupload) */ ?> - <section class="sc_box sc_modul_upload sc_modul_dynamic" v-if="state.html.checkoutDynamic" v-html="state.html.checkoutDynamic"></section> + <?php /* Schritt 3: Versandart */ ?> + <section class="sc_box" v-if="state.shipping.show" aria-labelledby="sc_title_shipping"> + <h2 class="sc_title" id="sc_title_shipping"><span class="sc_step">3</span><?php echo esc_html__('Versandart', 'wpsg'); ?></h2> - <?php /* Schritt 3: Versandart */ ?> - <section class="sc_box" v-if="state.shipping.show" aria-labelledby="sc_title_shipping"> - <h2 class="sc_title" id="sc_title_shipping"><span class="sc_step">3</span><?php echo esc_html__('Versandart', 'wpsg'); ?></h2> + <div class="sc_choices" :class="{ 'sc_invalid': hasError('shipping') }"> - <div class="sc_choices" :class="{ 'sc_invalid': hasError('shipping') }"> + <?php /* Das label umfasst nur die Kopfzeile, Hinweise der Module können eigene Eingabefelder enthalten */ ?> + <div class="sc_choice" v-for="item in state.shipping.list" :key="item.id" :class="{ 'sc_choice_active': form.shipping === item.id }" @click="selectChoice('shipping', item.id, $event)"> - <?php /* Das label umfasst nur die Kopfzeile, Hinweise der Module können eigene Eingabefelder enthalten */ ?> - <div class="sc_choice" v-for="item in state.shipping.list" :key="item.id" :class="{ 'sc_choice_active': form.shipping === item.id }" @click="selectChoice('shipping', item.id, $event)"> + <?php /* Radio, Logo und Bezeichnung auf einer Zeile, die Beschreibung darunter */ ?> + <label class="sc_choice_head"> + <input type="radio" name="wpsg[checkout][shipping]" :value="item.id" v-model="form.shipping" @change="refresh" /> + <img class="sc_choice_logo" v-if="item.logo" :src="item.logo" :alt="item.name" /> + <span class="sc_choice_name">{{ item.name }}</span> + <span class="sc_choice_price" v-if="item.price">{{ item.price }}</span> + </label> + <div class="sc_choice_hint" v-if="item.hint" v-html="item.hint"></div> - <?php /* Radio, Logo und Bezeichnung auf einer Zeile, die Beschreibung darunter */ ?> - <label class="sc_choice_head"> - <input type="radio" name="wpsg[checkout][shipping]" :value="item.id" v-model="form.shipping" @change="refresh" /> - <img class="sc_choice_logo" v-if="item.logo" :src="item.logo" :alt="item.name" /> - <span class="sc_choice_name">{{ item.name }}</span> - <span class="sc_choice_price" v-if="item.price">{{ item.price }}</span> - </label> - <div class="sc_choice_hint" v-if="item.hint" v-html="item.hint"></div> + </div> </div> - </div> + </section> - </section> + <?php /* Schritt 4: Zahlungsart */ ?> + <section class="sc_box" v-if="state.payment.show" aria-labelledby="sc_title_payment"> + <h2 class="sc_title" id="sc_title_payment"><span class="sc_step">4</span><?php echo esc_html__('Zahlungsart', 'wpsg'); ?></h2> - <?php /* Schritt 4: Zahlungsart */ ?> - <section class="sc_box" v-if="state.payment.show" aria-labelledby="sc_title_payment"> - <h2 class="sc_title" id="sc_title_payment"><span class="sc_step">4</span><?php echo esc_html__('Zahlungsart', 'wpsg'); ?></h2> + <div class="sc_choices" :class="{ 'sc_invalid': hasError('payment') }"> - <div class="sc_choices" :class="{ 'sc_invalid': hasError('payment') }"> + <?php /* Das label umfasst nur die Kopfzeile, Hinweise der Module können eigene Eingabefelder enthalten (z.B. Kreditkarte) */ ?> + <div class="sc_choice" v-for="item in state.payment.list" :key="item.id" :class="{ 'sc_choice_active': form.payment === item.id }" @click="selectChoice('payment', item.id, $event)"> - <?php /* Das label umfasst nur die Kopfzeile, Hinweise der Module können eigene Eingabefelder enthalten (z.B. Kreditkarte) */ ?> - <div class="sc_choice" v-for="item in state.payment.list" :key="item.id" :class="{ 'sc_choice_active': form.payment === item.id }" @click="selectChoice('payment', item.id, $event)"> + <?php /* Radio, Logo und Bezeichnung auf einer Zeile, Beschreibung und Zusatzangaben darunter */ ?> + <label class="sc_choice_head"> + <input type="radio" name="wpsg[checkout][payment]" :value="item.id" v-model="form.payment" @change="refresh" /> + <img class="sc_choice_logo" v-if="item.logo" :src="item.logo" :alt="item.name" /> + <span class="sc_choice_name">{{ item.name }}</span> + <span class="sc_choice_price" v-if="item.price">{{ item.price }}</span> + </label> + <div class="sc_choice_hint" v-if="item.hint" v-html="item.hint"></div> + <div class="sc_choice_extra" v-if="item.extra" v-html="item.extra"></div> - <?php /* Radio, Logo und Bezeichnung auf einer Zeile, Beschreibung und Zusatzangaben darunter */ ?> - <label class="sc_choice_head"> - <input type="radio" name="wpsg[checkout][payment]" :value="item.id" v-model="form.payment" @change="refresh" /> - <img class="sc_choice_logo" v-if="item.logo" :src="item.logo" :alt="item.name" /> - <span class="sc_choice_name">{{ item.name }}</span> - <span class="sc_choice_price" v-if="item.price">{{ item.price }}</span> - </label> - <div class="sc_choice_hint" v-if="item.hint" v-html="item.hint"></div> - <div class="sc_choice_extra" v-if="item.extra" v-html="item.extra"></div> + </div> </div> - </div> + <?php /* Zahlungsmodule, die eigene Eingabefelder mitbringen */ ?> + <div class="sc_modul_payment sc_modul_dynamic" v-if="state.html.paymentInner" v-html="state.html.paymentInner"></div> - <?php /* Zahlungsmodule, die eigene Eingabefelder mitbringen */ ?> - <div class="sc_modul_payment sc_modul_dynamic" v-if="state.html.paymentInner" v-html="state.html.paymentInner"></div> + </section> - </section> + <?php /* Bestellkommentar */ ?> + <section class="sc_box" aria-labelledby="sc_title_comment"> + <h2 class="sc_title" id="sc_title_comment"><?php echo esc_html__('Bestellkommentar', 'wpsg'); ?></h2> - <?php /* Bestellkommentar */ ?> - <section class="sc_box" aria-labelledby="sc_title_comment"> - <h2 class="sc_title" id="sc_title_comment"><?php echo esc_html__('Bestellkommentar', 'wpsg'); ?></h2> + <label class="sc_label sc_label_hidden" for="wpsg_sc_comment"><?php echo esc_html__('Bestellkommentar', 'wpsg'); ?></label> + <textarea class="sc_textarea" id="wpsg_sc_comment" name="wpsg[checkout][comment]" rows="4" v-model="form.comment"></textarea> - <label class="sc_label sc_label_hidden" for="wpsg_sc_comment"><?php echo esc_html__('Bestellkommentar', 'wpsg'); ?></label> - <textarea class="sc_textarea" id="wpsg_sc_comment" name="wpsg[checkout][comment]" rows="4" v-model="form.comment"></textarea> + </section> - </section> + </fieldset> </div> @@ -319,6 +331,14 @@ <h2 class="sc_title" id="sc_title_sum"><?php echo esc_html__('Zusammenfassung', 'wpsg'); ?></h2> + <?php /* Positionen in Kurzform, damit Ware, Menge und Preis unmittelbar über dem Bestellbutton stehen (§ 312j Abs. 2 BGB) */ ?> + <div class="sc_sum_products"> + <div class="sc_sum_product" v-for="product in state.products" :key="product.index"> + <span class="sc_sum_product_name">{{ product.amount }} × {{ product.name }}</span> + <span class="sc_sum_product_price">{{ product.price_total }}</span> + </div> + </div> + <div class="sc_sum_rows"> <div class="sc_sum_row"> @@ -398,11 +418,11 @@ <div class="sc_modul_conditions sc_modul_dynamic" :class="{ 'sc_invalid': state.errorConditions }" - v-if="state.html.conditions" + v-if="state.html.conditions && showFinal" v-html="state.html.conditions" ></div> - <div class="sc_agb" v-if="state.needAGB" :class="{ 'sc_invalid': hasError('agb') }"> + <div class="sc_agb" v-if="state.needAGB && showFinal" :class="{ 'sc_invalid': hasError('agb') }"> <label> <input type="checkbox" name="wpsg[agb]" value="1" v-model="agb" required /> <span><?php @@ -416,14 +436,35 @@ </div> <?php /* GARAN-Gewährleistungslabel direkt vor dem Bestellbutton */ ?> - <div class="sc_garan" v-if="state.warranty && state.warranty.need && !state.warranty.popup"> + <div class="sc_garan" v-if="state.warranty && state.warranty.need && !state.warranty.popup && showFinal"> <button type="button" class="sc_garan_zoom" @click="garanLightbox = true" title="<?php echo esc_attr__('Gewährleistungslabel vergrößern', 'wpsg'); ?>"> <img :src="state.warranty.image" alt="<?php echo esc_attr__('GARAN Gewährleistungslabel', 'wpsg'); ?>" width="1200" height="1697" /> </button> </div> + <?php /* Bestellabschluss „Zusammenfassung anzeigen“: Prüfen und weiter zur klassischen Zusammenfassung, dort wird bestellt */ ?> + <button + v-if="state.finish === 'overview'" + type="submit" + class="sc_button sc_button_order" + name="wpsg_smartcheckout_order" + value="1" + :disabled="busy" + ><?php echo esc_html__('Weiter zur Zusammenfassung', 'wpsg'); ?></button> + + <?php /* Bestellabschluss „Zusammenfassung vor Abschluss“: Prüfen und Eingaben für den Bestätigungsschritt sperren */ ?> + <button + v-if="state.canOrder && state.finish === 'confirm' && !confirm" + type="button" + class="sc_button sc_button_order" + @click="startConfirm" + :disabled="busy" + ><?php echo esc_html__('Weiter', 'wpsg'); ?></button> + + <input v-if="confirm" type="hidden" name="wpsg_smartcheckout_confirmed" value="1" /> + <button - v-if="state.canOrder" + v-if="state.canOrder && showFinal && state.finish !== 'overview'" type="submit" class="sc_button sc_button_order" name="wpsg_smartcheckout_order" @@ -435,6 +476,8 @@ <template v-else><?php echo esc_html__('zahlungspflichtig bestellen', 'wpsg'); ?></template> </button> + <button v-if="confirm" type="button" class="sc_button sc_button_secondary sc_button_edit" @click="editConfirm" :disabled="busy"><?php echo esc_html__('Angaben ändern', 'wpsg'); ?></button> + <div class="sc_sum_hint"> <?php echo wpsg_translate(__('Es gelten die Preise inklusive der gesetzlichen MwSt. zuzüglich #1#.', 'wpsg'), '<a target="_blank" rel="noopener" href="'.esc_url($state['url']['shipping']).'">'.esc_html__('Versandkosten', 'wpsg').'</a>'); ?> </div> diff --git a/views/mods/mod_smartcheckout/settings_edit.phtml b/views/mods/mod_smartcheckout/settings_edit.phtml index f713efe..46e43d8 100644 --- a/views/mods/mod_smartcheckout/settings_edit.phtml +++ b/views/mods/mod_smartcheckout/settings_edit.phtml @@ -10,17 +10,26 @@ '1' => __('Einspaltig', 'wpsg') ]; + // Reihenfolge wie im Bestellablauf: mit eigener Zusammenfassung zuerst + $arFinish = [ + wpsg_mod_smartcheckout::FINISH_OVERVIEW => __('Zusammenfassung anzeigen', 'wpsg'), + wpsg_mod_smartcheckout::FINISH_CONFIRM => __('Zusammenfassung vor Abschluss', 'wpsg'), + wpsg_mod_smartcheckout::FINISH_DIRECT => __('Direkt abschließen', 'wpsg') + ]; + ?> <?php echo wpsg_drawForm_Select('wpsg_mod_smartcheckout_page', __('Seite für den SmartCheckout', 'wpsg'), $this->view['arPages'], $this->get_option('wpsg_mod_smartcheckout_page'), ['hint' => __('Wählen Sie „Neu anlegen und zuordnen", damit beim Speichern automatisch eine eigene Seite erstellt wird — oder ordnen Sie eine bestehende Seite zu. Der Checkout wird automatisch in diese Seite eingebunden.', 'wpsg')]); ?> <?php echo wpsg_drawForm_Select('wpsg_mod_smartcheckout_layout', __('Aufteilung', 'wpsg'), $arLayout, $this->get_option('wpsg_mod_smartcheckout_layout'), ['hint' => __('Zweispaltig: Eingaben links, Zusammenfassung rechts mitlaufend. Einspaltig: Zusammenfassung unterhalb der Eingaben.', 'wpsg')]); ?> +<?php echo wpsg_drawForm_Select('wpsg_mod_smartcheckout_finish', __('Bestellabschluss', 'wpsg'), $arFinish, $this->get_option('wpsg_mod_smartcheckout_finish'), ['hint' => __('Zusammenfassung anzeigen: „Weiter zur Zusammenfassung“ führt auf die gesonderte Bestellübersicht, dort wird zahlungspflichtig bestellt. Zusammenfassung vor Abschluss: „Weiter“ prüft die Angaben und sperrt alle Felder, der Button wechselt auf „zahlungspflichtig bestellen“, mit „Angaben ändern“ lassen sich die Felder wieder bearbeiten. Direkt abschließen: Der Kunde bestellt direkt im SmartCheckout.', 'wpsg')]); ?> + <?php echo wpsg_drawForm_Checkbox('wpsg_mod_smartcheckout_image', __('Produktbilder im Warenkorb anzeigen', 'wpsg'), $this->get_option('wpsg_mod_smartcheckout_image')); ?> <p class="wpsg_note"> - <?php echo __('Solange das Modul aktiv ist, wird die Warenkorbseite auf den SmartCheckout umgeleitet — der klassische Ablauf (Warenkorb, Kasse, Versand/Zahlung, Zusammenfassung) wird nicht mehr angesteuert.', 'wpsg'); ?> + <?php echo esc_html__('Solange das Modul aktiv ist, wird die Warenkorbseite auf den SmartCheckout umgeleitet — der klassische Ablauf (Warenkorb, Kasse, Versand/Zahlung) wird nicht mehr angesteuert. Die klassische Zusammenfassung erscheint nur beim Bestellabschluss „Zusammenfassung anzeigen“.', 'wpsg'); ?> </p> <p class="wpsg_note"> - <?php echo __('Der SmartCheckout rechnet ausschließlich serverseitig — jede Änderung an Menge, Anschrift, Versand- oder Zahlungsart wird an den Shop geschickt und mit der regulären Kalkulation neu berechnet.', 'wpsg'); ?> + <?php echo esc_html__('Der SmartCheckout rechnet ausschließlich serverseitig — jede Änderung an Menge, Anschrift, Versand- oder Zahlungsart wird an den Shop geschickt und mit der regulären Kalkulation neu berechnet.', 'wpsg'); ?> </p> |
