From dcd11476826e8d5a99fbf74c96244570f619aca4 Mon Sep 17 00:00:00 2001
From: Michael Danielczok <michael@pcsg.de>
Date: Sun, 2 Mar 2025 16:17:13 +0100
Subject: [PATCH 1/2] feat: show errors direclty under the input

Improve error handling for coupon code input.
Added a catch block to handle errors when adding a coupon code to the basket.
The error message is now displayed directly under the input field, providing a better user experience.

Related: quiqqer/coupons#20
---
 ajax/frontend/redeem.php                   | 101 +++++++++------
 bin/frontend/controls/CouponCodeInput.css  |  19 ++-
 bin/frontend/controls/CouponCodeInput.html |   6 +-
 bin/frontend/controls/CouponCodeInput.js   | 142 ++++++++++++++++++---
 4 files changed, 208 insertions(+), 60 deletions(-)

diff --git a/ajax/frontend/redeem.php b/ajax/frontend/redeem.php
index 0866ff8..9eef139 100644
--- a/ajax/frontend/redeem.php
+++ b/ajax/frontend/redeem.php
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * This file contains package_quiqqer_coupons_ajax_delete
+ * This file contains package_quiqqer_coupons_ajax_frontend_redeem
  */
 
 use QUI\ERP\Coupons\Handler;
@@ -13,6 +13,15 @@
  * @param int $id - CouponCode ID
  * @return bool - success
  */
+
+/**
+ * Redeem a CouponCode
+ * @param string $code - coupon code
+ * @param string $orderHash - Order hash
+ *
+ * @throws QUI\Exception
+ * @throws QUI\ERP\Coupons\CouponCodeException
+ */
 QUI::$Ajax->registerFunction(
     'package_quiqqer_coupons_ajax_frontend_redeem',
     function ($code, $orderHash) {
@@ -23,20 +32,25 @@ function ($code, $orderHash) {
         } catch (QUI\ERP\Coupons\CouponCodeException $Exception) {
             QUI\System\Log::writeDebugException($Exception);
 
-            QUI::getMessagesHandler()->addError($Exception->getMessage());
-
-            return false;
+//            QUI::getMessagesHandler()->addError($Exception->getMessage());
+            throw $Exception;
+//            return false;
         } catch (Exception $Exception) {
             QUI\System\Log::writeException($Exception);
 
-            QUI::getMessagesHandler()->addError(
-                QUI::getLocale()->get(
-                    'quiqqer/coupons',
-                    'message.ajax.general_error'
-                )
-            );
+            throw new QUI\Exception([
+                'quiqqer/coupons',
+                'message.ajax.general_error'
+            ]);
 
-            return false;
+//            QUI::getMessagesHandler()->addError(
+//                QUI::getLocale()->get(
+//                    'quiqqer/coupons',
+//                    'message.ajax.general_error'
+//                )
+//            );
+
+//            return false;
         }
 
         $Order = QUI\ERP\Order\Handler::getInstance()->getOrderByHash($orderHash);
@@ -49,40 +63,55 @@ function ($code, $orderHash) {
 
         foreach ($discounts as $Discount) {
             if (!DiscountEvents::isDiscountUsableWithQuantity($Discount, $productCount)) {
-                QUI::getMessagesHandler()->addError(
-                    QUI::getLocale()->get(
-                        'quiqqer/coupons',
-                        'exception.CouponCode.discounts_invalid'
-                    )
-                );
-
-                return false;
+                throw new QUI\Exception([
+                    'quiqqer/coupons',
+                    'exception.CouponCode.discounts_invalid'
+                ]);
+
+//                QUI::getMessagesHandler()->addError(
+//                    QUI::getLocale()->get(
+//                        'quiqqer/coupons',
+//                        'exception.CouponCode.discounts_invalid'
+//                    )
+//                );
+//
+//                return false;
             }
 
             if ($Discount->getAttribute('scope') === QUI\ERP\Discount\Handler::DISCOUNT_SCOPE_GRAND_TOTAL) {
                 if (!DiscountEvents::isDiscountUsableWithPurchaseValue($Discount, $sum)) {
-                    QUI::getMessagesHandler()->addError(
-                        QUI::getLocale()->get(
-                            'quiqqer/coupons',
-                            'exception.CouponCode.discounts_invalid'
-                        )
-                    );
-
-                    return false;
+                    throw new QUI\Exception([
+                        'quiqqer/coupons',
+                        'exception.CouponCode.discounts_invalid'
+                    ]);
+
+//                    QUI::getMessagesHandler()->addError(
+//                        QUI::getLocale()->get(
+//                            'quiqqer/coupons',
+//                            'exception.CouponCode.discounts_invalid'
+//                        )
+//                    );
+//
+//                    return false;
                 }
 
                 continue;
             }
 
             if (!DiscountEvents::isDiscountUsableWithPurchaseValue($Discount, $subSum)) {
-                QUI::getMessagesHandler()->addError(
-                    QUI::getLocale()->get(
-                        'quiqqer/coupons',
-                        'exception.CouponCode.discounts_invalid'
-                    )
-                );
-
-                return false;
+                throw new QUI\Exception([
+                    'quiqqer/coupons',
+                    'exception.CouponCode.discounts_invalid'
+                ]);
+
+//                QUI::getMessagesHandler()->addError(
+//                    QUI::getLocale()->get(
+//                        'quiqqer/coupons',
+//                        'exception.CouponCode.discounts_invalid'
+//                    )
+//                );
+//
+//                return false;
             }
         }
 
@@ -105,7 +134,7 @@ function ($code, $orderHash) {
             $CouponCode->addToOrder($Order);
         }
 
-        return true;
+//        return true;
     },
     ['code', 'orderHash']
 );
diff --git a/bin/frontend/controls/CouponCodeInput.css b/bin/frontend/controls/CouponCodeInput.css
index cde8605..ebe6fcb 100644
--- a/bin/frontend/controls/CouponCodeInput.css
+++ b/bin/frontend/controls/CouponCodeInput.css
@@ -1,4 +1,6 @@
 .quiqqer-coupons-couponcodeinput {
+    --_qui-error-text-color: var(--qui-error-text-color, red);
+    --_qui-error-input-border-color: var(--qui-error-input-border-color, red);
     container-type: inline-size;
 }
 
@@ -14,13 +16,28 @@
 }
 
 .quiqqer-coupons-couponcodeinput-input {
-    min-width: min(250px, 100cqi);
+    min-width: min(200px, 100cqi);
 }
 
 .quiqqer-coupons-remove {
     margin-left: 1rem;
 }
 
+/* error handling */
+input.quiqqer-coupons-couponcodeinput-input[data-invalid] {
+    border-color: var(--_qui-error-input-border-color)
+}
+
+.quiqqer-coupons-couponcodeinput__errorMsg:not([data-show]) {
+    display: none;
+}
+
+:where(.quiqqer-coupons-couponcodeinput__errorMsg[data-show]) {
+    font-size: 0.875rem;
+    color: var(--_qui-error-text-color);
+    margin-top: 0.25rem;
+}
+
 /**
  * Simple Checkout
  */
diff --git a/bin/frontend/controls/CouponCodeInput.html b/bin/frontend/controls/CouponCodeInput.html
index ccc1a19..65b4dfc 100644
--- a/bin/frontend/controls/CouponCodeInput.html
+++ b/bin/frontend/controls/CouponCodeInput.html
@@ -4,11 +4,13 @@
     </label>
     <div class="quiqqer-coupons-couponcodeinput__inputGroup">
         <input class="quiqqer-coupons-couponcodeinput-input"
-               type="text" name="code-input" id="code-input" data-name="code-input"
+               type="text" name="code-input" id="code-input" data-ref="code-input"
                placeholder="{{labelInputPlaceholder}}"
+               autocomplete="off"
         />
-        <button class="quiqqer-coupons-couponcodeinput-btn btn btn-success" data-name="submit-coupon-code">
+        <button class="quiqqer-coupons-couponcodeinput-btn btn btn-success" data-ref="submit-coupon-code">
             {{submitBtnText}}
         </button>
     </div>
+    <div class="quiqqer-coupons-couponcodeinput__errorMsg" data-ref="errorMsg"></div>
 </div>
\ No newline at end of file
diff --git a/bin/frontend/controls/CouponCodeInput.js b/bin/frontend/controls/CouponCodeInput.js
index 91b4811..174c5c5 100644
--- a/bin/frontend/controls/CouponCodeInput.js
+++ b/bin/frontend/controls/CouponCodeInput.js
@@ -31,16 +31,19 @@ define('package/quiqqer/coupons/bin/frontend/controls/CouponCodeInput', [
         Type: 'package/quiqqer/coupons/bin/frontend/controls/CouponCodeInput',
 
         Binds: [
-            '$submit'
+            '$submit',
+            'handleError'
         ],
 
         initialize: function(options) {
             this.parent(options);
 
             this.$Input = null;
+            this.$ErrorMsgContainer = null;
             this.Loader = new QUILoader();
             this.$running = false;
 
+
             this.addEvents({
                 onInject: this.$onInject
             });
@@ -65,17 +68,42 @@ define('package/quiqqer/coupons/bin/frontend/controls/CouponCodeInput', [
                 submitBtnText: QUILocale.get(lg, lgPrefix + 'submitBtnText')
             }));
 
+            this.$ErrorMsgContainer = this.$Elm.querySelector('[data-ref="errorMsg"]');
+
             this.Loader.inject(this.$Elm);
 
-            this.$Input = this.$Elm.getElement('input[data-name="code-input"]');
+            this.$Input = this.$Elm.getElement('[data-ref="code-input"]');
+
+            let timeoutId = null;
+            let previousValue = '';
+            let currentValue = '';
+
+            const keyUpFunc = function(event ){
+                currentValue = self.$Input.value.trim();
 
-            this.$Input.addEvent('keyup', function(event) {
                 if (event.code === 13) {
+                    clearTimeout(timeoutId);
+                    previousValue = currentValue;
                     self.$submit();
+                } else {
+                    clearTimeout(timeoutId);
+
+                    timeoutId = setTimeout(function() {
+                        if (
+                            (currentValue !== previousValue || currentValue === '') &&
+                            self.$ErrorMsgContainer.getAttribute('data-show')
+                        ) {
+                            self.hideErrorMessage();
+                            self.resetInvalidInput();
+                        }
+                        previousValue = currentValue;
+                    }, 400);
                 }
-            });
+            };
+
+            this.$Input.addEvent('keyup', keyUpFunc);
 
-            this.$Elm.getElement('[data-name="submit-coupon-code"]').addEvent('click', function(event) {
+            this.$Elm.getElement('[data-ref="submit-coupon-code"]').addEvent('click', function(event) {
                 event.stop();
                 self.$submit();
             });
@@ -153,12 +181,7 @@ define('package/quiqqer/coupons/bin/frontend/controls/CouponCodeInput', [
 
                         this.$running = false;
                         this.Loader.hide();
-                    }).catch((err) => {
-                        console.error(err);
-
-                        this.$running = false;
-                        this.Loader.hide();
-                    });
+                    }).catch(self.handleError);
                 }, {
                     'package': 'quiqqer/order-simple-checkout',
                     orderHash: orderHash
@@ -184,7 +207,7 @@ define('package/quiqqer/coupons/bin/frontend/controls/CouponCodeInput', [
                                 'package': 'quiqqer/order'
                             });
                         });
-                    });
+                    }).catch(self.handleError);
                 }, {
                     'package': 'quiqqer/order'
                 });
@@ -196,19 +219,33 @@ define('package/quiqqer/coupons/bin/frontend/controls/CouponCodeInput', [
 
             OrderProcess.Loader.show();
 
+            // OrderProcess.getOrder().then(function(orderHash) {
+            //     return CouponCodes.addCouponCodeToBasket(code, orderHash);
+            // }).then(function(redeemed) {
+            //     self.$running = false;
+            //
+            //     if (redeemed === false) {
+            //         OrderProcess.Loader.hide();
+            //         self.Loader.hide();
+            //         return;
+            //     }
+            //
+            //     self.$addCouponCodeToSession(code).then(function() {
+            //         OrderProcess.reload();
+            //     });
+            // });
+
             OrderProcess.getOrder().then(function(orderHash) {
-                return CouponCodes.addCouponCodeToBasket(code, orderHash);
-            }).then(function(redeemed) {
-                self.$running = false;
+                CouponCodes.addCouponCodeToBasket(code, orderHash).then(() => {
+                    self.$running = false;
 
-                if (redeemed === false) {
+                    self.$addCouponCodeToSession(code).then(function() {
+                        OrderProcess.reload();
+                    });
+                }).catch((err) => {
                     OrderProcess.Loader.hide();
                     self.Loader.hide();
-                    return;
-                }
-
-                self.$addCouponCodeToSession(code).then(function() {
-                    OrderProcess.reload();
+                    self.handleError(err);
                 });
             });
         },
@@ -285,6 +322,69 @@ define('package/quiqqer/coupons/bin/frontend/controls/CouponCodeInput', [
                     });
                 });
             });
+        },
+
+        /**
+         * Show error message container
+         * @param msg
+         */
+        showErrorMessage: function( msg) {
+            if (!msg) {
+                console.log("no error message provided");
+
+                return;
+            }
+
+            if (!this.$ErrorMsgContainer) {
+                return;
+            }
+
+            this.$ErrorMsgContainer.set('html', msg);
+            this.$ErrorMsgContainer.setAttribute('data-show', '1');
+        },
+
+        /**
+         * Hide error message container
+         */
+        hideErrorMessage: function() {
+            if (!this.$ErrorMsgContainer) {
+                return;
+            }
+
+            this.$ErrorMsgContainer.removeAttribute('data-show');
+        },
+
+        /**
+         * Set data-invalid attribute to the input
+         */
+        highlightInvalidInput: function() {
+            this.$Input.setAttribute('data-invalid', '1');
+        },
+
+        /**
+         * Remove data-invalid attribute from the input
+         */
+        resetInvalidInput: function() {
+            this.$Input.removeAttribute('data-invalid');
+        },
+
+        /**
+         * Handle error message (show error message and highlight invalid input)
+         *
+         * @param err
+         */
+        handleError: function(err) {
+            console.error(err);
+
+            if (err.options !== undefined && err.options.message) {
+                const msg = err.options.message;
+                this.showErrorMessage(msg);
+                this.highlightInvalidInput();
+            }
+
+            this.$running = false;
+            this.Loader.hide();
         }
+
     });
 });
-- 
GitLab


From 400e67fac48c16049f14023568e42994c17313f8 Mon Sep 17 00:00:00 2001
From: Michael Danielczok <michael@pcsg.de>
Date: Tue, 4 Mar 2025 10:13:00 +0100
Subject: [PATCH 2/2] refactor: disabled quiqqer message on coupon code error

Related: quiqqer/coupons#20

Acked-by: Henning Leutz <leutz@pcsg.de>
---
 ajax/frontend/redeem.php                 | 40 ------------------------
 bin/frontend/classes/CouponCodes.js      |  3 +-
 bin/frontend/controls/CouponCodeInput.js | 17 ----------
 3 files changed, 2 insertions(+), 58 deletions(-)

diff --git a/ajax/frontend/redeem.php b/ajax/frontend/redeem.php
index 9eef139..02f080a 100644
--- a/ajax/frontend/redeem.php
+++ b/ajax/frontend/redeem.php
@@ -32,9 +32,7 @@ function ($code, $orderHash) {
         } catch (QUI\ERP\Coupons\CouponCodeException $Exception) {
             QUI\System\Log::writeDebugException($Exception);
 
-//            QUI::getMessagesHandler()->addError($Exception->getMessage());
             throw $Exception;
-//            return false;
         } catch (Exception $Exception) {
             QUI\System\Log::writeException($Exception);
 
@@ -42,15 +40,6 @@ function ($code, $orderHash) {
                 'quiqqer/coupons',
                 'message.ajax.general_error'
             ]);
-
-//            QUI::getMessagesHandler()->addError(
-//                QUI::getLocale()->get(
-//                    'quiqqer/coupons',
-//                    'message.ajax.general_error'
-//                )
-//            );
-
-//            return false;
         }
 
         $Order = QUI\ERP\Order\Handler::getInstance()->getOrderByHash($orderHash);
@@ -67,15 +56,6 @@ function ($code, $orderHash) {
                     'quiqqer/coupons',
                     'exception.CouponCode.discounts_invalid'
                 ]);
-
-//                QUI::getMessagesHandler()->addError(
-//                    QUI::getLocale()->get(
-//                        'quiqqer/coupons',
-//                        'exception.CouponCode.discounts_invalid'
-//                    )
-//                );
-//
-//                return false;
             }
 
             if ($Discount->getAttribute('scope') === QUI\ERP\Discount\Handler::DISCOUNT_SCOPE_GRAND_TOTAL) {
@@ -84,15 +64,6 @@ function ($code, $orderHash) {
                         'quiqqer/coupons',
                         'exception.CouponCode.discounts_invalid'
                     ]);
-
-//                    QUI::getMessagesHandler()->addError(
-//                        QUI::getLocale()->get(
-//                            'quiqqer/coupons',
-//                            'exception.CouponCode.discounts_invalid'
-//                        )
-//                    );
-//
-//                    return false;
                 }
 
                 continue;
@@ -103,15 +74,6 @@ function ($code, $orderHash) {
                     'quiqqer/coupons',
                     'exception.CouponCode.discounts_invalid'
                 ]);
-
-//                QUI::getMessagesHandler()->addError(
-//                    QUI::getLocale()->get(
-//                        'quiqqer/coupons',
-//                        'exception.CouponCode.discounts_invalid'
-//                    )
-//                );
-//
-//                return false;
             }
         }
 
@@ -133,8 +95,6 @@ function ($code, $orderHash) {
         if ($Order instanceof QUI\ERP\Order\OrderInProcess) {
             $CouponCode->addToOrder($Order);
         }
-
-//        return true;
     },
     ['code', 'orderHash']
 );
diff --git a/bin/frontend/classes/CouponCodes.js b/bin/frontend/classes/CouponCodes.js
index cea109e..d0076ba 100644
--- a/bin/frontend/classes/CouponCodes.js
+++ b/bin/frontend/classes/CouponCodes.js
@@ -32,7 +32,8 @@ define('package/quiqqer/coupons/bin/frontend/classes/CouponCodes', [
                     'package': pkg,
                     code: code,
                     orderHash: orderHash,
-                    onError: reject
+                    onError: reject,
+                    showError: false // disable quiqqer message in frontend
                 });
             });
         }
diff --git a/bin/frontend/controls/CouponCodeInput.js b/bin/frontend/controls/CouponCodeInput.js
index 174c5c5..df764ed 100644
--- a/bin/frontend/controls/CouponCodeInput.js
+++ b/bin/frontend/controls/CouponCodeInput.js
@@ -219,22 +219,6 @@ define('package/quiqqer/coupons/bin/frontend/controls/CouponCodeInput', [
 
             OrderProcess.Loader.show();
 
-            // OrderProcess.getOrder().then(function(orderHash) {
-            //     return CouponCodes.addCouponCodeToBasket(code, orderHash);
-            // }).then(function(redeemed) {
-            //     self.$running = false;
-            //
-            //     if (redeemed === false) {
-            //         OrderProcess.Loader.hide();
-            //         self.Loader.hide();
-            //         return;
-            //     }
-            //
-            //     self.$addCouponCodeToSession(code).then(function() {
-            //         OrderProcess.reload();
-            //     });
-            // });
-
             OrderProcess.getOrder().then(function(orderHash) {
                 CouponCodes.addCouponCodeToBasket(code, orderHash).then(() => {
                     self.$running = false;
@@ -244,7 +228,6 @@ define('package/quiqqer/coupons/bin/frontend/controls/CouponCodeInput', [
                     });
                 }).catch((err) => {
                     OrderProcess.Loader.hide();
-                    self.Loader.hide();
                     self.handleError(err);
                 });
             });
-- 
GitLab