Make order customer picker searchable and reload the list on open.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-11 18:17:56 +02:00
co-authored by Cursor
parent 5d70002345
commit 9d76af8963
3 changed files with 105 additions and 13 deletions
+74 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, nextTick, ref } from 'vue'
import { onClickOutside } from '@vueuse/core'
import AdminIcon from '@/components/icons/AdminIcon.vue'
@@ -15,11 +15,17 @@ const props = withDefaults(
placeholder?: string
ariaLabel?: string
block?: boolean
searchable?: boolean
searchPlaceholder?: string
id?: string
}>(),
{
placeholder: 'انتخاب کنید',
ariaLabel: 'انتخاب',
block: false,
searchable: false,
searchPlaceholder: 'جستجو…',
id: undefined,
},
)
@@ -28,28 +34,44 @@ const emit = defineEmits<{
}>()
const open = ref(false)
const searchQuery = ref('')
const rootRef = ref<HTMLElement | null>(null)
const searchInputRef = ref<HTMLInputElement | null>(null)
onClickOutside(rootRef, () => {
open.value = false
searchQuery.value = ''
})
const selectedLabel = computed(
() => props.options.find((option) => option.value === props.modelValue)?.label ?? props.placeholder,
)
function toggle() {
const filteredOptions = computed(() => {
const q = searchQuery.value.trim().toLowerCase()
if (!q) return props.options
return props.options.filter((option) => option.label.toLowerCase().includes(q))
})
async function toggle() {
open.value = !open.value
if (open.value && props.searchable) {
searchQuery.value = ''
await nextTick()
searchInputRef.value?.focus()
}
}
function pick(value: string) {
emit('update:modelValue', value)
open.value = false
searchQuery.value = ''
}
</script>
<template>
<div
:id="id"
ref="rootRef"
class="admin-select"
:class="{ 'admin-select--open': open, 'admin-select--block': block }"
@@ -74,7 +96,21 @@ function pick(value: string) {
role="listbox"
:aria-label="ariaLabel"
>
<li v-for="option in options" :key="option.value" role="presentation">
<li v-if="searchable" class="admin-select__search" role="presentation">
<input
ref="searchInputRef"
v-model="searchQuery"
type="search"
class="admin-select__search-input"
:placeholder="searchPlaceholder"
@click.stop
@keydown.esc.stop="open = false"
/>
</li>
<li v-if="!filteredOptions.length" class="admin-select__empty" role="presentation">
موردی یافت نشد
</li>
<li v-for="option in filteredOptions" :key="option.value" role="presentation">
<button
type="button"
class="admin-select__option"
@@ -169,6 +205,7 @@ function pick(value: string) {
top: calc(100% + 6px);
z-index: 40;
width: max(100%, 220px);
max-height: min(320px, 50vh);
margin: 0;
padding: 8px;
list-style: none;
@@ -179,6 +216,40 @@ function pick(value: string) {
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
}
.admin-select__search {
position: sticky;
top: 0;
z-index: 1;
background: var(--color-surface);
padding-bottom: 4px;
}
.admin-select__search-input {
width: 100%;
height: 36px;
padding: 0 12px;
border: 1px solid var(--color-border);
border-radius: 10px;
background: var(--color-bg);
color: var(--color-text);
font: inherit;
font-size: 0.8125rem;
}
.admin-select__search-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: var(--focus-ring);
}
.admin-select__empty {
padding: 12px;
font-size: 0.8125rem;
color: var(--color-text-muted);
text-align: center;
}
.admin-select__option {
+15 -1
View File
@@ -39,7 +39,11 @@ export function useOrdersPage() {
() => form.customerId.length > 0 && form.companyId.length > 0,
)
const activeCustomers = computed(() => customers.value.filter((c) => c.isActive))
const activeCustomers = computed(() => {
const list = [...customers.value]
list.sort((a, b) => Number(b.isActive) - Number(a.isActive) || a.name.localeCompare(b.name, 'fa'))
return list
})
function applyCustomer(customerId: string) {
const customer = customers.value.find((c) => c.id === customerId)
@@ -48,6 +52,14 @@ export function useOrdersPage() {
form.customerPhone = customer?.phone ?? ''
}
async function loadCustomers() {
try {
customers.value = await customersApi.list()
} catch (err) {
toast.error(err instanceof ApiError ? err.message : 'بارگذاری مشتریان انجام نشد.')
}
}
const filteredOrders = computed(() => {
let list = orderList.value
if (activeTab.value !== 'all') {
@@ -200,6 +212,7 @@ export function useOrdersPage() {
Object.assign(form, emptyForm())
formError.value = ''
showAddModal.value = true
void loadCustomers()
}
function closeAddModal() {
@@ -241,6 +254,7 @@ export function useOrdersPage() {
customers,
activeCustomers,
applyCustomer,
loadCustomers,
orderList,
activeTab,
selectedOrderId,
+16 -9
View File
@@ -21,12 +21,14 @@ const companyOptions = computed(() =>
)
const customerOptions = computed(() =>
activeCustomers.value.map((customer) => ({
value: customer.id,
label: customer.phone
? `${customer.name} — ${customer.phone}`
: customer.name,
})),
activeCustomers.value.map((customer) => {
const phone = customer.phone ? ` — ${customer.phone}` : ''
const inactive = customer.isActive === false ? ' (غیرفعال)' : ''
return {
value: customer.id,
label: `${customer.name}${phone}${inactive}`,
}
}),
)
const settlementOptions = [
@@ -53,16 +55,21 @@ watch(
<p v-if="formError" class="admin-form__error" role="alert">{{ formError }}</p>
<div class="admin-form__field">
<label>نام مشتری <span class="admin-form__required">*</span></label>
<label id="order-customer-label">
نام مشتری <span class="admin-form__required">*</span>
</label>
<AdminSelect
id="order-customer"
v-model="form.customerId"
block
aria-label="انتخاب مشتری"
searchable
aria-label="انتخاب مشتری از لیست"
placeholder="انتخاب مشتری از لیست"
search-placeholder="جستجوی نام یا موبایل…"
:options="customerOptions"
/>
<p v-if="!customerOptions.length" class="admin-form__hint">
مشتری فعالی یافت نشد. ابتدا از بخش مشتریان ثبت کنید.
مشتری‌ای در سیستم نیست. ابتدا از بخش «مشتریان» یک مشتری ثبت کنید.
</p>
</div>