feat(leads): gather filters in one popover like activities
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -360,7 +360,7 @@ function place() {
|
||||
|
||||
function liftAboveOverlays() {
|
||||
let z = 120;
|
||||
for (const el of document.querySelectorAll('.ant-modal-wrap, .ant-drawer')) {
|
||||
for (const el of document.querySelectorAll('.ant-modal-wrap, .ant-drawer, .ant-popover')) {
|
||||
const zi = Number.parseInt(window.getComputedStyle(el).zIndex, 10);
|
||||
if (Number.isFinite(zi)) z = Math.max(z, zi + 20);
|
||||
}
|
||||
|
||||
+432
-202
@@ -5,12 +5,10 @@ import axios from 'axios';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Avatar as AAvatar,
|
||||
Badge as ABadge,
|
||||
Button as AButton,
|
||||
Card as ACard,
|
||||
Checkbox as ACheckbox,
|
||||
Col as ACol,
|
||||
Drawer as ADrawer,
|
||||
Dropdown as ADropdown,
|
||||
Empty as AEmpty,
|
||||
Flex as AFlex,
|
||||
@@ -24,6 +22,7 @@ import {
|
||||
Modal,
|
||||
Modal as AModal,
|
||||
Pagination as APagination,
|
||||
Popover as APopover,
|
||||
RadioButton as ARadioButton,
|
||||
RadioGroup as ARadioGroup,
|
||||
Row as ARow,
|
||||
@@ -41,6 +40,7 @@ import type { TableColumnsType, UploadProps } from 'ant-design-vue';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
EditOutlined,
|
||||
ExportOutlined,
|
||||
FileExcelOutlined,
|
||||
@@ -55,6 +55,7 @@ import { api } from '@/lib/api';
|
||||
import {
|
||||
formatJalaliNumeric,
|
||||
formatRelativeContact,
|
||||
parseLocalInput,
|
||||
toFaDigits,
|
||||
} from '@/lib/jalali';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
@@ -298,8 +299,12 @@ const assigneeSelectValue = computed(() => {
|
||||
if (unassigned.value) return UNASSIGNED_VALUE;
|
||||
return assigneeId.value || undefined;
|
||||
});
|
||||
const moreFiltersCount = computed(() =>
|
||||
/** شمارش فیلترهای داخل پاپآور، مثل فعالیتها. */
|
||||
const mobileFilterBadgeCount = computed(() =>
|
||||
[
|
||||
stageId.value,
|
||||
assigneeSelectValue.value ? '1' : '',
|
||||
followUpFilter.value,
|
||||
cityFilter.value,
|
||||
regionFilter.value,
|
||||
tradeFilter.value,
|
||||
@@ -310,17 +315,204 @@ const moreFiltersCount = computed(() =>
|
||||
convertedFilter.value ? '1' : '',
|
||||
].filter(Boolean).length,
|
||||
);
|
||||
/** در موبایل مرتبسازی و مسئول و پیگیری هم داخل کشو هستند. */
|
||||
const mobileFilterBadgeCount = computed(() => {
|
||||
const extra = isMobile.value
|
||||
? [
|
||||
sort.value !== 'updated' ? '1' : '',
|
||||
assigneeSelectValue.value ? '1' : '',
|
||||
followUpFilter.value,
|
||||
].filter(Boolean).length
|
||||
: 0;
|
||||
return moreFiltersCount.value + extra;
|
||||
});
|
||||
const filterBtnLabel = computed(() =>
|
||||
mobileFilterBadgeCount.value
|
||||
? `فیلتر (${toFaDigits(mobileFilterBadgeCount.value)})`
|
||||
: 'فیلتر',
|
||||
);
|
||||
|
||||
type LeadFacetKey = 'assignee' | 'stage' | 'source' | 'followUp' | 'city' | 'region' | 'trade';
|
||||
const filterSection = ref<LeadFacetKey | 'createdFrom' | 'createdTo' | 'converted' | 'archived' | null>(
|
||||
null,
|
||||
);
|
||||
const filterQuery = ref('');
|
||||
const FACET_LIST_CAP = 40;
|
||||
|
||||
function normFilter(value: string) {
|
||||
return value.trim().replace(/ي/g, 'ی').replace(/ك/g, 'ک').toLowerCase();
|
||||
}
|
||||
|
||||
function toggleFilterSection(name: NonNullable<typeof filterSection.value>) {
|
||||
filterSection.value = filterSection.value === name ? null : name;
|
||||
filterQuery.value = '';
|
||||
}
|
||||
|
||||
const filterAnchor = ref<HTMLElement | null>(null);
|
||||
const filterPopoverMaxHeight = ref('min(70vh, 32rem)');
|
||||
|
||||
function placeFilterPopover() {
|
||||
const el = filterAnchor.value;
|
||||
if (!el) return;
|
||||
const space = Math.floor(window.innerHeight - el.getBoundingClientRect().bottom - 12);
|
||||
filterPopoverMaxHeight.value = `${Math.max(220, space)}px`;
|
||||
}
|
||||
|
||||
function onFilterPopover(open: boolean) {
|
||||
if (!open && document.getElementById('jalali-picker')) return;
|
||||
if (open) placeFilterPopover();
|
||||
moreFiltersOpen.value = open;
|
||||
if (open) {
|
||||
window.addEventListener('resize', placeFilterPopover);
|
||||
window.addEventListener('scroll', placeFilterPopover, true);
|
||||
} else {
|
||||
filterSection.value = null;
|
||||
window.removeEventListener('resize', placeFilterPopover);
|
||||
window.removeEventListener('scroll', placeFilterPopover, true);
|
||||
}
|
||||
}
|
||||
|
||||
function dateFacetLabel(value: string) {
|
||||
if (!value) return 'همه';
|
||||
const parsed = parseLocalInput(value);
|
||||
return parsed ? formatJalaliNumeric(parsed) : value;
|
||||
}
|
||||
|
||||
function facetValueStyle(active: boolean) {
|
||||
return {
|
||||
color: active ? token.value.colorPrimary : token.value.colorTextQuaternary,
|
||||
fontWeight: active ? 700 : 400,
|
||||
maxWidth: '9.5rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
} as const;
|
||||
}
|
||||
|
||||
const textFacets = computed(() => [
|
||||
{
|
||||
key: 'assignee' as const,
|
||||
label: 'کارشناس',
|
||||
value: unassigned.value
|
||||
? 'بدون کارشناس'
|
||||
: users.value.find((user) => user.id === assigneeId.value)?.fullName || 'همه',
|
||||
active: Boolean(assigneeSelectValue.value),
|
||||
current: assigneeSelectValue.value || '',
|
||||
searchable: true,
|
||||
options: [
|
||||
{ value: '', label: 'همه' },
|
||||
{ value: UNASSIGNED_VALUE, label: 'بدون کارشناس' },
|
||||
...users.value.map((user) => ({ value: user.id, label: user.fullName })),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'stage' as const,
|
||||
label: 'مرحله',
|
||||
value: stages.value.find((stage) => stage.id === stageId.value)?.name || 'همه',
|
||||
active: Boolean(stageId.value),
|
||||
current: stageId.value,
|
||||
searchable: stages.value.length > 8,
|
||||
options: [
|
||||
{ value: '', label: 'همه' },
|
||||
...stages.value.map((stage) => ({ value: stage.id, label: stage.name })),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'source' as const,
|
||||
label: 'منبع',
|
||||
value: sourceFilter.value || 'همه',
|
||||
active: Boolean(sourceFilter.value),
|
||||
current: sourceFilter.value,
|
||||
searchable: sourceOptions.value.length > 8,
|
||||
options: [
|
||||
{ value: '', label: 'همه' },
|
||||
...sourceOptions.value.map((item) => ({ value: item, label: item })),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'followUp' as const,
|
||||
label: 'پیگیری',
|
||||
value:
|
||||
FOLLOW_UP_OPTIONS.find((item) => item.value === followUpFilter.value && item.value)?.label ||
|
||||
'همه',
|
||||
active: Boolean(followUpFilter.value),
|
||||
current: followUpFilter.value,
|
||||
searchable: false,
|
||||
options: [
|
||||
{ value: '', label: 'همه' },
|
||||
...FOLLOW_UP_OPTIONS.filter((item) => item.value).map((item) => ({
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
})),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'city' as const,
|
||||
label: 'شهر',
|
||||
value: cityFilter.value || 'همه',
|
||||
active: Boolean(cityFilter.value),
|
||||
current: cityFilter.value,
|
||||
searchable: cityOptions.value.length > 8,
|
||||
options: [
|
||||
{ value: '', label: 'همه' },
|
||||
...cityOptions.value.map((item) => ({ value: item, label: item })),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'region' as const,
|
||||
label: 'منطقه',
|
||||
value: regionFilter.value || 'همه',
|
||||
active: Boolean(regionFilter.value),
|
||||
current: regionFilter.value,
|
||||
searchable: regionOptions.value.length > 8,
|
||||
options: [
|
||||
{ value: '', label: 'همه' },
|
||||
...regionOptions.value.map((item) => ({ value: item, label: item })),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'trade' as const,
|
||||
label: 'صنف',
|
||||
value: tradeFilter.value || 'همه',
|
||||
active: Boolean(tradeFilter.value),
|
||||
current: tradeFilter.value,
|
||||
searchable: tradeOptions.value.length > 8,
|
||||
options: [
|
||||
{ value: '', label: 'همه' },
|
||||
...tradeOptions.value.map((item) => ({ value: item, label: item })),
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
function visibleFacetOptions(options: Array<{ value: string; label: string }>) {
|
||||
const query = normFilter(filterQuery.value);
|
||||
const rest = options.filter(
|
||||
(item) => item.value !== '' && (!query || normFilter(item.label).includes(query)),
|
||||
);
|
||||
return {
|
||||
options: [...options.filter((item) => item.value === ''), ...rest.slice(0, FACET_LIST_CAP)],
|
||||
hidden: Math.max(0, rest.length - FACET_LIST_CAP),
|
||||
empty: Boolean(query) && rest.length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
function pickFacet(key: LeadFacetKey, value: string) {
|
||||
const facet = textFacets.value.find((item) => item.key === key);
|
||||
const next = facet && facet.current === value ? '' : value;
|
||||
if (key === 'assignee') setAssigneeFilter(next || undefined);
|
||||
else if (key === 'stage') setStageFilter(next);
|
||||
else if (key === 'source') setSourceFilter(next);
|
||||
else if (key === 'followUp') setFollowUpFilter(next || undefined);
|
||||
else if (key === 'city') setCityFilter(next);
|
||||
else if (key === 'region') setRegionFilter(next);
|
||||
else if (key === 'trade') setTradeFilter(next);
|
||||
filterSection.value = null;
|
||||
}
|
||||
|
||||
function pickFlag(which: 'converted' | 'archived', on: boolean) {
|
||||
const current = which === 'converted' ? convertedFilter.value : archivedFilter.value;
|
||||
const next = on ? !current : false;
|
||||
if (which === 'converted') setConvertedFilter(next);
|
||||
else setArchivedFilter(next);
|
||||
filterSection.value = null;
|
||||
}
|
||||
|
||||
function facetOptionStyle(selected: boolean) {
|
||||
return {
|
||||
textAlign: 'start' as const,
|
||||
background: selected ? token.value.colorPrimaryBg : 'transparent',
|
||||
color: selected ? token.value.colorPrimary : token.value.colorText,
|
||||
};
|
||||
}
|
||||
|
||||
const total = computed(() => data.value?.total ?? 0);
|
||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)));
|
||||
@@ -647,6 +839,10 @@ function setConvertedFilter(checked: boolean) {
|
||||
|
||||
function clearMoreFilters() {
|
||||
patchQuery({
|
||||
stageId: undefined,
|
||||
assigneeId: undefined,
|
||||
unassigned: undefined,
|
||||
followUp: undefined,
|
||||
city: undefined,
|
||||
region: undefined,
|
||||
trade: undefined,
|
||||
@@ -655,16 +851,9 @@ function clearMoreFilters() {
|
||||
createdTo: undefined,
|
||||
archived: undefined,
|
||||
converted: undefined,
|
||||
...(isMobile.value
|
||||
? {
|
||||
sort: undefined,
|
||||
assigneeId: undefined,
|
||||
unassigned: undefined,
|
||||
followUp: undefined,
|
||||
}
|
||||
: {}),
|
||||
page: undefined,
|
||||
});
|
||||
filterSection.value = null;
|
||||
}
|
||||
|
||||
function setPage(next: number) {
|
||||
@@ -1342,6 +1531,8 @@ onMounted(async () => {
|
||||
onBeforeUnmount(() => {
|
||||
stopImportPoll();
|
||||
revokeLogoUrls();
|
||||
window.removeEventListener('resize', placeFilterPopover);
|
||||
window.removeEventListener('scroll', placeFilterPopover, true);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1507,12 +1698,12 @@ onBeforeUnmount(() => {
|
||||
</a-space>
|
||||
</a-flex>
|
||||
|
||||
<a-flex v-if="isMobile" vertical :gap="8">
|
||||
<a-flex :vertical="isMobile" align="center" wrap="wrap" :gap="8">
|
||||
<a-input-search
|
||||
v-model:value="q"
|
||||
allow-clear
|
||||
placeholder="جستجو نام، موبایل، شهر…"
|
||||
style="width: 100%"
|
||||
:style="isMobile ? { width: '100%' } : { width: '15rem', maxWidth: '100%' }"
|
||||
@search="onSearch"
|
||||
>
|
||||
<template #enterButton>
|
||||
@@ -1521,195 +1712,234 @@ onBeforeUnmount(() => {
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input-search>
|
||||
<a-badge
|
||||
:count="mobileFilterBadgeCount"
|
||||
:offset="[-2, 2]"
|
||||
:style="{ width: '100%', display: 'block' }"
|
||||
>
|
||||
<a-button block @click="moreFiltersOpen = true">
|
||||
<template #icon><FilterOutlined /></template>
|
||||
فیلترها
|
||||
</a-button>
|
||||
</a-badge>
|
||||
</a-flex>
|
||||
<a-flex v-else align="center" wrap="wrap" :gap="8">
|
||||
<a-input-search
|
||||
v-model:value="q"
|
||||
allow-clear
|
||||
placeholder="جستجو نام، موبایل، شهر…"
|
||||
style="width: 15rem; max-width: 100%"
|
||||
@search="onSearch"
|
||||
>
|
||||
<template #enterButton>
|
||||
<a-button type="primary">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
<div ref="filterAnchor" :style="isMobile ? { width: '100%' } : undefined">
|
||||
<a-popover
|
||||
:open="moreFiltersOpen"
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
:arrow="false"
|
||||
:auto-adjust-overflow="false"
|
||||
:overlay-inner-style="{
|
||||
width: 'min(20rem, calc(100vw - 1.5rem))',
|
||||
maxHeight: filterPopoverMaxHeight,
|
||||
overflow: 'auto',
|
||||
}"
|
||||
@open-change="onFilterPopover"
|
||||
>
|
||||
<a-button
|
||||
:block="isMobile"
|
||||
:style="mobileFilterBadgeCount
|
||||
? {
|
||||
borderColor: token.colorPrimary,
|
||||
color: token.colorPrimary,
|
||||
background: token.colorPrimaryBg,
|
||||
}
|
||||
: undefined"
|
||||
>
|
||||
<a-flex align="center" justify="center" :gap="6">
|
||||
<FilterOutlined />
|
||||
<span>{{ filterBtnLabel }}</span>
|
||||
<DownOutlined />
|
||||
</a-flex>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input-search>
|
||||
<a-badge :count="mobileFilterBadgeCount" :offset="[-2, 2]">
|
||||
<a-button @click="moreFiltersOpen = true">
|
||||
<template #icon><FilterOutlined /></template>
|
||||
فیلترها
|
||||
</a-button>
|
||||
</a-badge>
|
||||
<template #content>
|
||||
<a-flex vertical :gap="10" @click.stop>
|
||||
<div v-for="facet in textFacets" :key="facet.key">
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="toggleFilterSection(facet.key)"
|
||||
@keydown.enter.prevent="toggleFilterSection(facet.key)"
|
||||
>
|
||||
<a-flex justify="space-between" align="center">
|
||||
<span>{{ facet.label }}</span>
|
||||
<a-typography-text :style="facetValueStyle(facet.active)">
|
||||
{{ facet.value }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</div>
|
||||
<a-flex
|
||||
v-if="filterSection === facet.key"
|
||||
vertical
|
||||
:gap="4"
|
||||
:style="{ marginTop: '6px' }"
|
||||
>
|
||||
<a-input
|
||||
v-if="facet.searchable"
|
||||
v-model:value="filterQuery"
|
||||
allow-clear
|
||||
placeholder="جستجو"
|
||||
/>
|
||||
<div :style="{ maxHeight: '11.5rem', overflow: 'auto' }">
|
||||
<a-button
|
||||
v-for="option in visibleFacetOptions(facet.options).options"
|
||||
:key="option.value || 'all'"
|
||||
type="text"
|
||||
block
|
||||
:style="facetOptionStyle(facet.current === option.value)"
|
||||
@click="pickFacet(facet.key, option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</a-button>
|
||||
<a-typography-text
|
||||
v-if="visibleFacetOptions(facet.options).empty"
|
||||
type="secondary"
|
||||
>
|
||||
موردی پیدا نشد
|
||||
</a-typography-text>
|
||||
</div>
|
||||
<a-typography-text
|
||||
v-if="visibleFacetOptions(facet.options).hidden"
|
||||
type="secondary"
|
||||
>
|
||||
برای دیدن بقیه، نام را دقیقتر بنویسید
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="toggleFilterSection('createdFrom')"
|
||||
@keydown.enter.prevent="toggleFilterSection('createdFrom')"
|
||||
>
|
||||
<a-flex justify="space-between" align="center">
|
||||
<span>از تاریخ ثبت</span>
|
||||
<a-typography-text :style="facetValueStyle(Boolean(createdFromFilter))">
|
||||
{{ dateFacetLabel(createdFromFilter) }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</div>
|
||||
<div v-if="filterSection === 'createdFrom'" :style="{ marginTop: '6px' }">
|
||||
<JalaliDateInput
|
||||
:model-value="createdFromFilter"
|
||||
placeholder="از تاریخ"
|
||||
@update:model-value="(value: string) => { setCreatedFromFilter(value); filterSection = null }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="toggleFilterSection('createdTo')"
|
||||
@keydown.enter.prevent="toggleFilterSection('createdTo')"
|
||||
>
|
||||
<a-flex justify="space-between" align="center">
|
||||
<span>تا تاریخ ثبت</span>
|
||||
<a-typography-text :style="facetValueStyle(Boolean(createdToFilter))">
|
||||
{{ dateFacetLabel(createdToFilter) }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</div>
|
||||
<div v-if="filterSection === 'createdTo'" :style="{ marginTop: '6px' }">
|
||||
<JalaliDateInput
|
||||
:model-value="createdToFilter"
|
||||
placeholder="تا تاریخ"
|
||||
@update:model-value="(value: string) => { setCreatedToFilter(value); filterSection = null }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="toggleFilterSection('converted')"
|
||||
@keydown.enter.prevent="toggleFilterSection('converted')"
|
||||
>
|
||||
<a-flex justify="space-between" align="center">
|
||||
<span>تبدیلشده</span>
|
||||
<a-typography-text :style="facetValueStyle(convertedFilter)">
|
||||
{{ convertedFilter ? 'فقط' : 'همه' }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</div>
|
||||
<a-flex v-if="filterSection === 'converted'" vertical :style="{ marginTop: '6px' }">
|
||||
<a-button
|
||||
type="text"
|
||||
block
|
||||
:style="facetOptionStyle(!convertedFilter)"
|
||||
@click="pickFlag('converted', false)"
|
||||
>
|
||||
همه
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
block
|
||||
:style="facetOptionStyle(convertedFilter)"
|
||||
@click="pickFlag('converted', true)"
|
||||
>
|
||||
فقط تبدیلشدهها
|
||||
</a-button>
|
||||
</a-flex>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:style="{ cursor: 'pointer' }"
|
||||
@click="toggleFilterSection('archived')"
|
||||
@keydown.enter.prevent="toggleFilterSection('archived')"
|
||||
>
|
||||
<a-flex justify="space-between" align="center">
|
||||
<span>بایگانی</span>
|
||||
<a-typography-text :style="facetValueStyle(archivedFilter)">
|
||||
{{ archivedFilter ? 'فقط' : 'همه' }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
</div>
|
||||
<a-flex v-if="filterSection === 'archived'" vertical :style="{ marginTop: '6px' }">
|
||||
<a-button
|
||||
type="text"
|
||||
block
|
||||
:style="facetOptionStyle(!archivedFilter)"
|
||||
@click="pickFlag('archived', false)"
|
||||
>
|
||||
همه
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
block
|
||||
:style="facetOptionStyle(archivedFilter)"
|
||||
@click="pickFlag('archived', true)"
|
||||
>
|
||||
فقط بایگانیشدهها
|
||||
</a-button>
|
||||
</a-flex>
|
||||
</div>
|
||||
|
||||
<a-button
|
||||
v-if="mobileFilterBadgeCount"
|
||||
type="text"
|
||||
danger
|
||||
block
|
||||
@click="clearMoreFilters"
|
||||
>
|
||||
پاک کردن فیلترها
|
||||
</a-button>
|
||||
</a-flex>
|
||||
</template>
|
||||
</a-popover>
|
||||
</div>
|
||||
<a-select
|
||||
:value="sort"
|
||||
style="width: 10rem"
|
||||
:style="isMobile ? { width: '100%' } : { width: '10rem' }"
|
||||
:options="Object.entries(sortLabels).map(([value, label]) => ({ value, label }))"
|
||||
@update:value="(v) => setSort(String(v ?? 'updated') as SortKey)"
|
||||
/>
|
||||
<a-select
|
||||
:value="assigneeSelectValue"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="مسئول"
|
||||
style="width: 10rem"
|
||||
:options="[
|
||||
{ value: UNASSIGNED_VALUE, label: 'بدون مسئول' },
|
||||
...users.map((u) => ({ value: u.id, label: u.fullName })),
|
||||
]"
|
||||
@update:value="(v) => setAssigneeFilter(v == null ? undefined : String(v))"
|
||||
/>
|
||||
<a-select
|
||||
:value="followUpFilter || undefined"
|
||||
allow-clear
|
||||
placeholder="پیگیری"
|
||||
style="width: 13rem"
|
||||
:options="FOLLOW_UP_OPTIONS.filter((o) => o.value).map((o) => ({ value: o.value, label: o.label }))"
|
||||
@update:value="(v) => setFollowUpFilter(v == null ? undefined : String(v))"
|
||||
/>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-drawer
|
||||
v-model:open="moreFiltersOpen"
|
||||
:title="isMobile ? 'فیلترها' : 'فیلترهای بیشتر'"
|
||||
placement="left"
|
||||
:width="isMobile ? '100%' : 380"
|
||||
:destroy-on-close="false"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<template v-if="isMobile">
|
||||
<a-form-item label="مرتبسازی">
|
||||
<a-select
|
||||
:value="sort"
|
||||
style="width: 100%"
|
||||
:options="Object.entries(sortLabels).map(([value, label]) => ({ value, label }))"
|
||||
@update:value="(v) => setSort(String(v ?? 'updated') as SortKey)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="مسئول">
|
||||
<a-select
|
||||
:value="assigneeSelectValue"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="همه مسئولان"
|
||||
style="width: 100%"
|
||||
:options="[
|
||||
{ value: UNASSIGNED_VALUE, label: 'بدون مسئول' },
|
||||
...users.map((u) => ({ value: u.id, label: u.fullName })),
|
||||
]"
|
||||
@update:value="(v) => setAssigneeFilter(v == null ? undefined : String(v))"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="پیگیری">
|
||||
<a-select
|
||||
:value="followUpFilter || undefined"
|
||||
allow-clear
|
||||
placeholder="همه پیگیریها"
|
||||
style="width: 100%"
|
||||
:options="FOLLOW_UP_OPTIONS.filter((o) => o.value).map((o) => ({ value: o.value, label: o.label }))"
|
||||
@update:value="(v) => setFollowUpFilter(v == null ? undefined : String(v))"
|
||||
/>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="شهر">
|
||||
<a-select
|
||||
:value="cityFilter || undefined"
|
||||
allow-clear
|
||||
show-search
|
||||
placeholder="همه شهرها"
|
||||
:options="cityOptions.map((c) => ({ value: c, label: c }))"
|
||||
@update:value="(v) => setCityFilter(v == null ? '' : String(v))"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="منطقه">
|
||||
<a-select
|
||||
:value="regionFilter || undefined"
|
||||
allow-clear
|
||||
show-search
|
||||
placeholder="همه مناطق"
|
||||
:options="regionOptions.map((r) => ({ value: r, label: r }))"
|
||||
@update:value="(v) => setRegionFilter(v == null ? '' : String(v))"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="صنف">
|
||||
<a-select
|
||||
:value="tradeFilter || undefined"
|
||||
allow-clear
|
||||
show-search
|
||||
placeholder="همه اصناف"
|
||||
:options="tradeOptions.map((t) => ({ value: t, label: t }))"
|
||||
@update:value="(v) => setTradeFilter(v == null ? '' : String(v))"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="منبع">
|
||||
<a-select
|
||||
:value="sourceFilter || undefined"
|
||||
allow-clear
|
||||
show-search
|
||||
placeholder="همه منابع"
|
||||
:options="sourceOptions.map((s) => ({ value: s, label: s }))"
|
||||
@update:value="(v) => setSourceFilter(v == null ? '' : String(v))"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="از تاریخ ثبت">
|
||||
<JalaliDateInput
|
||||
:model-value="createdFromFilter"
|
||||
placeholder="از تاریخ"
|
||||
@update:model-value="setCreatedFromFilter"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="تا تاریخ ثبت">
|
||||
<JalaliDateInput
|
||||
:model-value="createdToFilter"
|
||||
placeholder="تا تاریخ"
|
||||
@update:model-value="setCreatedToFilter"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox
|
||||
:checked="convertedFilter"
|
||||
@update:checked="setConvertedFilter"
|
||||
>
|
||||
فقط تبدیلشدهها
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox
|
||||
:checked="archivedFilter"
|
||||
@update:checked="setArchivedFilter"
|
||||
>
|
||||
فقط بایگانیشدهها
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button :disabled="!mobileFilterBadgeCount" @click="clearMoreFilters">
|
||||
پاک کردن
|
||||
</a-button>
|
||||
<a-button type="primary" @click="moreFiltersOpen = false">
|
||||
بستن
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<a-col v-if="selectedIds.length" :span="24" @click.stop>
|
||||
<a-card :bordered="false" :style="{ background: token.colorPrimaryBg }">
|
||||
<a-space :size="8" wrap>
|
||||
|
||||
Reference in New Issue
Block a user