From 040cbd196205d494e2de638ed64b11bd78ae9a8c Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Mon, 23 Mar 2026 21:21:36 +0100 Subject: [PATCH] feat(loyalty): add customer autocomplete to terminal search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terminal search now shows live autocomplete suggestions as the user types (debounced 300ms, min 2 chars). Dropdown shows matching customers with avatar, name, email, card number, and points balance. Uses the existing GET /store/loyalty/cards?search= endpoint (limit=5). Selecting a result loads the full card details via the lookup endpoint. Enter key still works for exact lookup. No new dependencies — uses native Alpine.js dropdown, no Tom Select needed. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../static/store/js/loyalty-terminal.js | 55 +++++++++++++++++++ .../templates/loyalty/store/terminal.html | 39 +++++++++++-- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/app/modules/loyalty/static/store/js/loyalty-terminal.js b/app/modules/loyalty/static/store/js/loyalty-terminal.js index 8ea4ed16..11553964 100644 --- a/app/modules/loyalty/static/store/js/loyalty-terminal.js +++ b/app/modules/loyalty/static/store/js/loyalty-terminal.js @@ -23,6 +23,9 @@ function storeLoyaltyTerminal() { searchQuery: '', lookingUp: false, selectedCard: null, + searchResults: [], + showSearchDropdown: false, + _searchTimeout: null, // Transaction inputs earnAmount: null, @@ -146,6 +149,58 @@ function storeLoyaltyTerminal() { } }, + // Debounced search for autocomplete suggestions + debouncedSearchCustomers() { + if (this._searchTimeout) clearTimeout(this._searchTimeout); + if (!this.searchQuery || this.searchQuery.length < 2) { + this.searchResults = []; + this.showSearchDropdown = false; + return; + } + this._searchTimeout = setTimeout(() => this.searchCustomers(), 300); + }, + + async searchCustomers() { + try { + const params = new URLSearchParams({ + search: this.searchQuery, + limit: '5', + is_active: 'true' + }); + const response = await apiClient.get(`/store/loyalty/cards?${params}`); + if (response && response.cards) { + this.searchResults = response.cards; + this.showSearchDropdown = this.searchResults.length > 0; + } + } catch (error) { + loyaltyTerminalLog.warn('Search failed:', error.message); + this.searchResults = []; + this.showSearchDropdown = false; + } + }, + + // Select a customer from autocomplete dropdown + async selectCustomer(card) { + this.showSearchDropdown = false; + this.searchResults = []; + this.lookingUp = true; + + try { + // Use the lookup endpoint to get full card details + const response = await apiClient.get(`/store/loyalty/cards/lookup?q=${encodeURIComponent(card.card_number)}`); + if (response) { + this.selectedCard = response; + this.searchQuery = ''; + loyaltyTerminalLog.info('Customer selected:', this.selectedCard.customer_name); + } + } catch (error) { + Utils.showToast(I18n.t('loyalty.store.terminal.error_lookup', {message: error.message}), 'error'); + loyaltyTerminalLog.error('Lookup failed:', error); + } finally { + this.lookingUp = false; + } + }, + // Clear selected customer clearCustomer() { this.selectedCard = null; diff --git a/app/modules/loyalty/templates/loyalty/store/terminal.html b/app/modules/loyalty/templates/loyalty/store/terminal.html index 53bc4153..ebcca41c 100644 --- a/app/modules/loyalty/templates/loyalty/store/terminal.html +++ b/app/modules/loyalty/templates/loyalty/store/terminal.html @@ -63,18 +63,49 @@
- -
- + +
+ + +
+ +