From 14ebf2316c24b56f695b27aac92270596095c5bb Mon Sep 17 00:00:00 2001
From: Michael Danielczok <michael@pcsg.de>
Date: Tue, 4 Mar 2025 09:27:19 +0000
Subject: [PATCH] feat: show errors directly under the input

Acked-by: Henning Leutz <leutz@pcsg.de>
---
 ajax/frontend/redeem.php                   |  65 +++++------
 bin/frontend/classes/CouponCodes.js        |   3 +-
 bin/frontend/controls/CouponCodeInput.css  |  19 ++-
 bin/frontend/controls/CouponCodeInput.html |   6 +-
 bin/frontend/controls/CouponCodeInput.js   | 127 +++++++++++++++++----
 5 files changed, 156 insertions(+), 64 deletions(-)

diff --git a/ajax/frontend/redeem.php b/ajax/frontend/redeem.php
index 0866ff8..02f080a 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,14 @@ function ($code, $orderHash) {
         } catch (QUI\ERP\Coupons\CouponCodeException $Exception) {
             QUI\System\Log::writeDebugException($Exception);
 
-            QUI::getMessagesHandler()->addError($Exception->getMessage());
-
-            return false;
+            throw $Exception;
         } catch (Exception $Exception) {
             QUI\System\Log::writeException($Exception);
 
-            QUI::getMessagesHandler()->addError(
-                QUI::getLocale()->get(
-                    'quiqqer/coupons',
-                    'message.ajax.general_error'
-                )
-            );
-
-            return false;
+            throw new QUI\Exception([
+                'quiqqer/coupons',
+                'message.ajax.general_error'
+            ]);
         }
 
         $Order = QUI\ERP\Order\Handler::getInstance()->getOrderByHash($orderHash);
@@ -49,40 +52,28 @@ 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'
+                ]);
             }
 
             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'
+                    ]);
                 }
 
                 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'
+                ]);
             }
         }
 
@@ -104,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.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..df764ed 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'
                 });
@@ -197,18 +220,15 @@ 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;
+                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 +305,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