refactor(loyalty): use search_autocomplete macro for staff PIN lookup

Replace custom inline autocomplete HTML in both create and edit PIN
modals with the shared search_autocomplete macro from inputs.html.
Refactored JS to use staffSearchResults array populated by searchStaff()
(client-side filter) matching the macro's conventions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 21:30:10 +01:00
parent fcde2d68fc
commit 518bace534
2 changed files with 53 additions and 60 deletions

View File

@@ -60,35 +60,40 @@ function loyaltyPinsList(config) {
// Staff autocomplete state
staffSearch: '',
staffSearchResults: [],
showStaffDropdown: false,
// Track which member was selected (to detect when user edits away)
searchingStaff: false,
_selectedStaffName: '',
get filteredStaff() {
if (!this.staffSearch) return this.staffMembers;
searchStaff() {
// Client-side filter of loaded staff members
if (!this.staffSearch || this.staffSearch.length < 1) {
this.staffSearchResults = [];
this.showStaffDropdown = false;
return;
}
const q = this.staffSearch.toLowerCase();
return this.staffMembers.filter(m =>
this.staffSearchResults = this.staffMembers.filter(m =>
(m.full_name && m.full_name.toLowerCase().includes(q)) ||
(m.email && m.email.toLowerCase().includes(q))
);
},
this.showStaffDropdown = this.staffSearchResults.length > 0;
selectStaffMember(member) {
this.pinForm.name = member.full_name;
this.pinForm.staff_id = member.email;
this.staffSearch = member.full_name;
this._selectedStaffName = member.full_name;
this.showStaffDropdown = false;
},
onStaffSearchInput() {
this.pinForm.name = this.staffSearch;
// If user modified the text away from the selected member, clear staff_id
if (this._selectedStaffName && this.staffSearch !== this._selectedStaffName) {
this.pinForm.staff_id = '';
this._selectedStaffName = '';
}
this.pinForm.name = this.staffSearch;
},
selectStaffMember(item) {
this.pinForm.name = item.full_name;
this.pinForm.staff_id = item.email;
this.staffSearch = item.full_name;
this._selectedStaffName = item.full_name;
this.showStaffDropdown = false;
this.staffSearchResults = [];
},
clearStaffSelection() {
@@ -97,6 +102,7 @@ function loyaltyPinsList(config) {
this.pinForm.staff_id = '';
this._selectedStaffName = '';
this.showStaffDropdown = false;
this.staffSearchResults = [];
},
// Action state

View File

@@ -9,6 +9,7 @@
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/tables.html' import table_wrapper, table_header %}
{% from 'shared/macros/modals.html' import modal, confirm_modal %}
{% from 'shared/macros/inputs.html' import search_autocomplete %}
<!-- Stats Summary -->
<div x-show="!loading" class="mb-6 flex flex-wrap items-center gap-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
@@ -132,29 +133,22 @@
<form @submit.prevent="createPin()">
<div class="space-y-4">
<!-- Staff Member Autocomplete -->
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_name') }}</label>
<input type="text" x-model="staffSearch" required
@focus="open = staffSearch.length > 0 && staffMembers.length > 0"
@input="open = staffMembers.length > 0; onStaffSearchInput()"
autocomplete="off"
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
placeholder="{{ _('loyalty.shared.pins.pin_name') }}">
<!-- Dropdown -->
<div x-show="open && filteredStaff.length > 0" x-cloak
class="absolute z-50 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-48 overflow-y-auto">
<template x-for="member in filteredStaff" :key="member.id">
<button type="button"
@click="selectStaffMember(member); open = false"
class="w-full px-3 py-2 text-left text-sm hover:bg-purple-50 dark:hover:bg-gray-600 flex items-center justify-between">
<span>
<span class="font-medium text-gray-800 dark:text-gray-200" x-text="member.full_name"></span>
<span class="text-gray-500 dark:text-gray-400 text-xs ml-2" x-text="member.role_name"></span>
</span>
<span class="text-xs text-gray-400" x-text="member.email"></span>
</button>
</template>
</div>
{{ search_autocomplete(
search_var='staffSearch',
results_var='staffSearchResults',
show_dropdown_var='showStaffDropdown',
loading_var='searchingStaff',
search_action='searchStaff()',
select_action='selectStaffMember(item)',
display_field='full_name',
secondary_field='email',
placeholder=_('loyalty.shared.pins.pin_name'),
min_chars=1,
no_results_text=_('loyalty.store.terminal.customer_not_found'),
loading_text=_('loyalty.store.terminal.looking_up')
) }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_staff_id') }}</label>
@@ -201,29 +195,22 @@
<form @submit.prevent="updatePin()">
<div class="space-y-4">
<!-- Staff Member Autocomplete -->
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_name') }}</label>
<input type="text" x-model="staffSearch" required
@focus="open = staffSearch.length > 0 && staffMembers.length > 0"
@input="open = staffMembers.length > 0; onStaffSearchInput()"
autocomplete="off"
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
placeholder="{{ _('loyalty.shared.pins.pin_name') }}">
<!-- Dropdown -->
<div x-show="open && filteredStaff.length > 0" x-cloak
class="absolute z-50 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-48 overflow-y-auto">
<template x-for="member in filteredStaff" :key="member.id">
<button type="button"
@click="selectStaffMember(member); open = false"
class="w-full px-3 py-2 text-left text-sm hover:bg-purple-50 dark:hover:bg-gray-600 flex items-center justify-between">
<span>
<span class="font-medium text-gray-800 dark:text-gray-200" x-text="member.full_name"></span>
<span class="text-gray-500 dark:text-gray-400 text-xs ml-2" x-text="member.role_name"></span>
</span>
<span class="text-xs text-gray-400" x-text="member.email"></span>
</button>
</template>
</div>
{{ search_autocomplete(
search_var='staffSearch',
results_var='staffSearchResults',
show_dropdown_var='showStaffDropdown',
loading_var='searchingStaff',
search_action='searchStaff()',
select_action='selectStaffMember(item)',
display_field='full_name',
secondary_field='email',
placeholder=_('loyalty.shared.pins.pin_name'),
min_chars=1,
no_results_text=_('loyalty.store.terminal.customer_not_found'),
loading_text=_('loyalty.store.terminal.looking_up')
) }}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_staff_id') }}</label>