Make order customer picker searchable and reload the list on open.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user