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:
2026-03-23 20:58:10 +01:00
parent 316ec42566
commit 4429674100
3 changed files with 98 additions and 10 deletions

View File

@@ -133,17 +133,37 @@
{% call modal('createPinModal', _('loyalty.shared.pins.create_pin'), 'showCreateModal', size='md', show_footer=false) %}
<form @submit.prevent="createPin()">
<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>
<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"
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>
<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
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_staff_id') }}">
<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 bg-gray-50 dark:bg-gray-800"
placeholder="{{ _('loyalty.shared.pins.pin_staff_id') }}"
:readonly="pinForm.staff_id && staffMembers.length > 0">
</div>
<div>
<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) %}
<form @submit.prevent="updatePin()">
<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>
<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"
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>
<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
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_staff_id') }}">
<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 bg-gray-50 dark:bg-gray-800"
placeholder="{{ _('loyalty.shared.pins.pin_staff_id') }}"
:readonly="pinForm.staff_id && staffMembers.length > 0">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('loyalty.shared.pins.pin_code') }}</label>