feat(loyalty): add staff autocomplete to PIN management
When creating or editing a staff PIN in the store context, the name field now shows an autocomplete dropdown with the store's team members (loaded from GET /store/team/members). Selecting a member auto-fills name and staff_id (email). The dropdown filters as you type. Only active in store context (where staffApiPrefix is configured). Merchant and admin PIN views are unaffected — merchant has no staffApiPrefix, admin is read-only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ function loyaltyPinsList(config) {
|
|||||||
pins: [],
|
pins: [],
|
||||||
program: null,
|
program: null,
|
||||||
locations: [],
|
locations: [],
|
||||||
|
staffMembers: [],
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
filters: {
|
filters: {
|
||||||
@@ -57,6 +58,33 @@ function loyaltyPinsList(config) {
|
|||||||
store_id: ''
|
store_id: ''
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Staff autocomplete state
|
||||||
|
staffSearch: '',
|
||||||
|
showStaffDropdown: false,
|
||||||
|
|
||||||
|
get filteredStaff() {
|
||||||
|
if (!this.staffSearch) return this.staffMembers;
|
||||||
|
const q = this.staffSearch.toLowerCase();
|
||||||
|
return this.staffMembers.filter(m =>
|
||||||
|
(m.full_name && m.full_name.toLowerCase().includes(q)) ||
|
||||||
|
(m.email && m.email.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectStaffMember(member) {
|
||||||
|
this.pinForm.name = member.full_name;
|
||||||
|
this.pinForm.staff_id = member.email;
|
||||||
|
this.staffSearch = member.full_name;
|
||||||
|
this.showStaffDropdown = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearStaffSelection() {
|
||||||
|
this.staffSearch = '';
|
||||||
|
this.pinForm.name = '';
|
||||||
|
this.pinForm.staff_id = '';
|
||||||
|
this.showStaffDropdown = false;
|
||||||
|
},
|
||||||
|
|
||||||
// Action state
|
// Action state
|
||||||
saving: false,
|
saving: false,
|
||||||
deleting: false,
|
deleting: false,
|
||||||
@@ -88,6 +116,9 @@ function loyaltyPinsList(config) {
|
|||||||
if (config.showStoreFilter) {
|
if (config.showStoreFilter) {
|
||||||
parallel.push(this.loadLocations());
|
parallel.push(this.loadLocations());
|
||||||
}
|
}
|
||||||
|
if (config.showCrud && config.staffApiPrefix) {
|
||||||
|
parallel.push(this.loadStaffMembers());
|
||||||
|
}
|
||||||
await Promise.all(parallel);
|
await Promise.all(parallel);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -137,6 +168,18 @@ function loyaltyPinsList(config) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async loadStaffMembers() {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(config.staffApiPrefix + '/team/members');
|
||||||
|
if (response && response.members) {
|
||||||
|
this.staffMembers = response.members.filter(m => m.is_active);
|
||||||
|
loyaltyPinsListLog.info('Loaded', this.staffMembers.length, 'staff members');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
loyaltyPinsListLog.warn('Failed to load staff members:', error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
computeStats() {
|
computeStats() {
|
||||||
this.stats.total = this.pins.length;
|
this.stats.total = this.pins.length;
|
||||||
this.stats.active = this.pins.filter(p => p.is_active && !p.is_locked).length;
|
this.stats.active = this.pins.filter(p => p.is_active && !p.is_locked).length;
|
||||||
@@ -156,6 +199,8 @@ function loyaltyPinsList(config) {
|
|||||||
pin: '',
|
pin: '',
|
||||||
store_id: ''
|
store_id: ''
|
||||||
};
|
};
|
||||||
|
this.staffSearch = '';
|
||||||
|
this.showStaffDropdown = false;
|
||||||
this.showCreateModal = true;
|
this.showCreateModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -167,6 +212,8 @@ function loyaltyPinsList(config) {
|
|||||||
pin: '',
|
pin: '',
|
||||||
store_id: pin.store_id || ''
|
store_id: pin.store_id || ''
|
||||||
};
|
};
|
||||||
|
this.staffSearch = pin.name || '';
|
||||||
|
this.showStaffDropdown = false;
|
||||||
this.showEditModal = true;
|
this.showEditModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const storePinsLog = window.LogConfig.loggers.storePins || window.LogConfig.crea
|
|||||||
function storeLoyaltyPins() {
|
function storeLoyaltyPins() {
|
||||||
return loyaltyPinsList({
|
return loyaltyPinsList({
|
||||||
apiPrefix: '/store/loyalty',
|
apiPrefix: '/store/loyalty',
|
||||||
|
staffApiPrefix: '/store',
|
||||||
showStoreFilter: false,
|
showStoreFilter: false,
|
||||||
showCrud: true,
|
showCrud: true,
|
||||||
currentPage: 'pins',
|
currentPage: 'pins',
|
||||||
|
|||||||
@@ -133,17 +133,37 @@
|
|||||||
{% call modal('createPinModal', _('loyalty.shared.pins.create_pin'), 'showCreateModal', size='md', show_footer=false) %}
|
{% call modal('createPinModal', _('loyalty.shared.pins.create_pin'), 'showCreateModal', size='md', show_footer=false) %}
|
||||||
<form @submit.prevent="createPin()">
|
<form @submit.prevent="createPin()">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<!-- Staff Member Autocomplete -->
|
||||||
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_name') }}</label>
|
<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="pinForm.name" required
|
<input type="text" x-model="staffSearch" required
|
||||||
|
@focus="open = staffMembers.length > 0"
|
||||||
|
@input="open = staffMembers.length > 0; pinForm.name = staffSearch"
|
||||||
|
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"
|
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') }}">
|
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>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_staff_id') }}</label>
|
||||||
<input type="text" x-model="pinForm.staff_id" required
|
<input type="text" x-model="pinForm.staff_id"
|
||||||
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"
|
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 bg-gray-50 dark:bg-gray-800"
|
||||||
placeholder="{{ _('loyalty.shared.pins.pin_staff_id') }}">
|
placeholder="{{ _('loyalty.shared.pins.pin_staff_id') }}"
|
||||||
|
:readonly="pinForm.staff_id && staffMembers.length > 0">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_code') }}</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_code') }}</label>
|
||||||
@@ -182,17 +202,37 @@
|
|||||||
{% call modal('editPinModal', _('loyalty.shared.pins.edit_pin'), 'showEditModal', size='md', show_footer=false) %}
|
{% call modal('editPinModal', _('loyalty.shared.pins.edit_pin'), 'showEditModal', size='md', show_footer=false) %}
|
||||||
<form @submit.prevent="updatePin()">
|
<form @submit.prevent="updatePin()">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<!-- Staff Member Autocomplete -->
|
||||||
|
<div class="relative" x-data="{ open: false }" @click.outside="open = false">
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_name') }}</label>
|
<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="pinForm.name" required
|
<input type="text" x-model="staffSearch" required
|
||||||
|
@focus="open = staffMembers.length > 0"
|
||||||
|
@input="open = staffMembers.length > 0; pinForm.name = staffSearch"
|
||||||
|
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"
|
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') }}">
|
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>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_staff_id') }}</label>
|
||||||
<input type="text" x-model="pinForm.staff_id" required
|
<input type="text" x-model="pinForm.staff_id"
|
||||||
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"
|
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 bg-gray-50 dark:bg-gray-800"
|
||||||
placeholder="{{ _('loyalty.shared.pins.pin_staff_id') }}">
|
placeholder="{{ _('loyalty.shared.pins.pin_staff_id') }}"
|
||||||
|
:readonly="pinForm.staff_id && staffMembers.length > 0">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_code') }}</label>
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_code') }}</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user